diff --git a/.github/workflows/build-docker-mariadb.yml b/.github/workflows/build-docker-mariadb.yml index a127570a965..46e165b8935 100644 --- a/.github/workflows/build-docker-mariadb.yml +++ b/.github/workflows/build-docker-mariadb.yml @@ -7,7 +7,7 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: DEVELOCITY_ACCESS_KEY: ${{ secrets.GE_ACCESS_TOKEN }} @@ -21,7 +21,7 @@ jobs: java-version: '17' distribution: 'zulu' - name: Setup Gradle - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 - name: Build the image run: ./gradlew --no-daemon --console=plain :fineract-provider:clean :fineract-provider:build :fineract-provider:jibDockerBuild -x test -x cucumber - name: Start the stack diff --git a/.github/workflows/build-docker-postgresql.yml b/.github/workflows/build-docker-postgresql.yml index 70b19a79ca0..15c194e875f 100644 --- a/.github/workflows/build-docker-postgresql.yml +++ b/.github/workflows/build-docker-postgresql.yml @@ -7,7 +7,7 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: DEVELOCITY_ACCESS_KEY: ${{ secrets.GE_ACCESS_TOKEN }} @@ -21,7 +21,7 @@ jobs: java-version: '17' distribution: 'zulu' - name: Setup Gradle - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 - name: Build the image run: ./gradlew --no-daemon --console=plain :fineract-provider:clean :fineract-provider:build :fineract-provider:jibDockerBuild -x test -x cucumber - name: Start the Standalone Stack diff --git a/.github/workflows/build-documentation.yml b/.github/workflows/build-documentation.yml index 8f3d532f644..c67db14130f 100644 --- a/.github/workflows/build-documentation.yml +++ b/.github/workflows/build-documentation.yml @@ -6,7 +6,7 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: DEVELOCITY_ACCESS_KEY: ${{ secrets.GE_ACCESS_TOKEN }} steps: @@ -20,7 +20,7 @@ jobs: java-version: '17' distribution: 'zulu' - name: Setup Gradle - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: 16 diff --git a/.github/workflows/build-mariadb.yml b/.github/workflows/build-mariadb.yml index 0e95a585b4e..9aa8d8e6947 100644 --- a/.github/workflows/build-mariadb.yml +++ b/.github/workflows/build-mariadb.yml @@ -6,7 +6,7 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 services: mariad: @@ -44,7 +44,7 @@ jobs: - name: Congfigure vega-cli run: npm i -g vega-cli --unsafe - name: Setup Gradle and Validate Wrapper - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 with: validate-wrappers: true - name: Verify MariaDB connection diff --git a/.github/workflows/build-mysql.yml b/.github/workflows/build-mysql.yml index 37d510e222b..124b92ebd1a 100644 --- a/.github/workflows/build-mysql.yml +++ b/.github/workflows/build-mysql.yml @@ -6,11 +6,11 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 services: mariad: - image: mysql:8.4 + image: mysql:9.1 ports: - 3306:3306 env: @@ -44,7 +44,7 @@ jobs: - name: Congfigure vega-cli run: npm i -g vega-cli --unsafe - name: Setup Gradle and Validate Wrapper - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 with: validate-wrappers: true - name: Verify MariaDB connection diff --git a/.github/workflows/build-postgresql.yml b/.github/workflows/build-postgresql.yml index acf63d889f9..eba477b86d1 100644 --- a/.github/workflows/build-postgresql.yml +++ b/.github/workflows/build-postgresql.yml @@ -6,7 +6,7 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 services: postgresql: @@ -45,7 +45,7 @@ jobs: - name: Congfigure vega-cli run: npm i -g vega-cli --unsafe - name: Setup Gradle and Validate Wrapper - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 with: validate-wrappers: true - name: Verify PostgreSQL connection diff --git a/.github/workflows/build-tests.yml b/.github/workflows/build-tests.yml index 9d817fd50b5..faae209ae4d 100644 --- a/.github/workflows/build-tests.yml +++ b/.github/workflows/build-tests.yml @@ -7,7 +7,7 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: DEVELOCITY_ACCESS_KEY: ${{ secrets.GE_ACCESS_TOKEN }} @@ -21,7 +21,7 @@ jobs: java-version: '17' distribution: 'zulu' - name: Setup Gradle - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 - name: Build the image run: ./gradlew --no-daemon --console=plain :fineract-provider:clean :fineract-provider:build :fineract-provider:jibDockerBuild -x test -x cucumber - name: Start the Fineract stack diff --git a/.github/workflows/publish-dockerhub.yml b/.github/workflows/publish-dockerhub.yml index 296df5d1efa..b19b295873a 100644 --- a/.github/workflows/publish-dockerhub.yml +++ b/.github/workflows/publish-dockerhub.yml @@ -12,7 +12,7 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: DEVELOCITY_ACCESS_KEY: ${{ secrets.GE_ACCESS_TOKEN }} @@ -27,7 +27,7 @@ jobs: java-version: '17' distribution: 'zulu' - name: Setup Gradle - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 - name: Extract branch name shell: bash run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT diff --git a/.github/workflows/smoke-activemq.yml b/.github/workflows/smoke-activemq.yml index 824a72b3271..39f55c38352 100644 --- a/.github/workflows/smoke-activemq.yml +++ b/.github/workflows/smoke-activemq.yml @@ -7,7 +7,7 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: DEVELOCITY_ACCESS_KEY: ${{ secrets.GE_ACCESS_TOKEN }} @@ -21,7 +21,7 @@ jobs: java-version: '17' distribution: 'zulu' - name: Setup Gradle - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 - name: Build the image run: ./gradlew --no-daemon --console=plain :fineract-provider:clean :fineract-provider:build :fineract-provider:jibDockerBuild -x test -x cucumber - name: Start the ActiveMQ Stack diff --git a/.github/workflows/smoke-kafka.yml b/.github/workflows/smoke-kafka.yml index aeab45597f2..e25379ba0f2 100644 --- a/.github/workflows/smoke-kafka.yml +++ b/.github/workflows/smoke-kafka.yml @@ -7,7 +7,7 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: DEVELOCITY_ACCESS_KEY: ${{ secrets.GE_ACCESS_TOKEN }} @@ -21,7 +21,7 @@ jobs: java-version: '17' distribution: 'zulu' - name: Setup Gradle - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 - name: Build the image run: ./gradlew --no-daemon --console=plain :fineract-provider:clean :fineract-provider:build :fineract-provider:jibDockerBuild -x test -x cucumber - name: Start the Kafka Stack diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index b4fe625ae70..750ac10d5cd 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -9,7 +9,7 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: TZ: Asia/Kolkata SONAR_ORGANIZATION: ${{ secrets.SONAR_ORGANIZATION }} @@ -29,7 +29,7 @@ jobs: java-version: '17' distribution: 'zulu' - name: Setup Gradle and Validate Wrapper - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 with: validate-wrappers: true - name: Sonarqube diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java index 8dd35f89150..43680cb5b75 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java @@ -63,10 +63,13 @@ public class ProductToGLAccountMapping extends AbstractPersistableCustom { @Column(name = "financial_account_type", nullable = true) private int financialAccountType; + @Column(name = "charge_off_reason_id", nullable = true) + private Long chargeOffReasonId; + public static ProductToGLAccountMapping createNew(final GLAccount glAccount, final Long productId, final int productType, - final int financialAccountType) { + final int financialAccountType, final Long chargeOffReasonId) { return new ProductToGLAccountMapping().setGlAccount(glAccount).setProductId(productId).setProductType(productType) - .setFinancialAccountType(financialAccountType); + .setFinancialAccountType(financialAccountType).setChargeOffReasonId(chargeOffReasonId); } } diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java index 1a7ecc8dfac..fa953838f7f 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java @@ -35,7 +35,7 @@ ProductToGLAccountMapping findProductIdAndProductTypeAndFinancialAccountTypeAndC @Param("productType") int productType, @Param("financialAccountType") int financialAccountType, @Param("chargeId") Long ChargeId); - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL") + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReasonId is NULL") ProductToGLAccountMapping findCoreProductToFinAccountMapping(@Param("productId") Long productId, @Param("productType") int productType, @Param("financialAccountType") int financialAccountType); @@ -61,4 +61,8 @@ List findAllPenaltyToIncomeAccountMappings(@Param("pr @Param("productType") int productType); List findByProductIdAndProductType(Long productId, int productType); + + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.chargeOffReasonId is not NULL") + List findAllChargesOffReasonsMappings(@Param("productId") Long productId, + @Param("productType") int productType); } diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java index f51ad5c578d..5c01b16706b 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan; @@ -39,7 +40,11 @@ import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingInvalidException; import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingNotFoundException; +import org.apache.fineract.infrastructure.codes.domain.CodeValue; +import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.charge.domain.Charge; @@ -53,6 +58,7 @@ public class ProductToGLAccountMappingHelper { protected static final List ASSET_LIABILITY_TYPES = List.of(GLAccountType.ASSET, GLAccountType.LIABILITY); + private static final Integer GL_ACCOUNT_EXPENSE_TYPE = 5; protected final GLAccountRepository accountRepository; protected final ProductToGLAccountMappingRepository accountMappingRepository; @@ -60,6 +66,7 @@ public class ProductToGLAccountMappingHelper { private final ChargeRepositoryWrapper chargeRepositoryWrapper; protected final GLAccountRepositoryWrapper accountRepositoryWrapper; private final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper; + private final CodeValueRepository codeValueRepository; public void saveProductToAccountMapping(final JsonElement element, final String paramName, final Long productId, final int placeHolderTypeId, final GLAccountType expectedAccountType, final PortfolioProductType portfolioProductType) { @@ -194,6 +201,27 @@ public void saveChargesToGLAccountMappings(final JsonCommand command, final Json } } + public void saveChargeOffReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, + final Map changes, final PortfolioProductType portfolioProductType) { + + final String arrayName = LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(); + final JsonArray chargeOffReasonToExpenseAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayName, element); + + if (chargeOffReasonToExpenseAccountMappingArray != null) { + if (changes != null) { + changes.put(arrayName, command.jsonFragment(arrayName)); + } + + for (int i = 0; i < chargeOffReasonToExpenseAccountMappingArray.size(); i++) { + final JsonObject jsonObject = chargeOffReasonToExpenseAccountMappingArray.get(i).getAsJsonObject(); + final Long reasonId = jsonObject.get(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue()).getAsLong(); + final Long expenseAccountId = jsonObject.get(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()).getAsLong(); + + saveChargeOffReasonToExpenseMapping(productId, reasonId, expenseAccountId, portfolioProductType); + } + } + } + /** * @param command * @param element @@ -356,6 +384,65 @@ public void updatePaymentChannelToFundSourceMappings(final JsonCommand command, } } + public void updateChargeOffReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, + final Map changes, final PortfolioProductType portfolioProductType) { + + final List existingChargeOffReasonToGLAccountMappings = this.accountMappingRepository + .findAllChargesOffReasonsMappings(productId, portfolioProductType.getValue()); + final JsonArray chargeOffReasonToGLAccountMappingArray = this.fromApiJsonHelper + .extractJsonArrayNamed(LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(), element); + + final Map inputChargeOffReasonToGLAccountMap = new HashMap<>(); + + final Set existingChargeOffReasons = new HashSet<>(); + if (chargeOffReasonToGLAccountMappingArray != null) { + if (changes != null) { + changes.put(LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(), + command.jsonFragment(LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue())); + } + + for (int i = 0; i < chargeOffReasonToGLAccountMappingArray.size(); i++) { + final JsonObject jsonObject = chargeOffReasonToGLAccountMappingArray.get(i).getAsJsonObject(); + final Long expenseGlAccountId = jsonObject.get(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()).getAsLong(); + final Long chargeOffReasonCodeValueId = jsonObject + .get(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue()).getAsLong(); + inputChargeOffReasonToGLAccountMap.put(chargeOffReasonCodeValueId, expenseGlAccountId); + } + + // If input map is empty, delete all existing mappings + if (inputChargeOffReasonToGLAccountMap.isEmpty()) { + this.accountMappingRepository.deleteAllInBatch(existingChargeOffReasonToGLAccountMappings); + } else { + for (final ProductToGLAccountMapping existingChargeOffReasonToGLAccountMapping : existingChargeOffReasonToGLAccountMappings) { + final Long currentChargeOffReasonId = existingChargeOffReasonToGLAccountMapping.getChargeOffReasonId(); + if (currentChargeOffReasonId != null) { + existingChargeOffReasons.add(currentChargeOffReasonId); + // update existing mappings (if required) + if (inputChargeOffReasonToGLAccountMap.containsKey(currentChargeOffReasonId)) { + final Long newGLAccountId = inputChargeOffReasonToGLAccountMap.get(currentChargeOffReasonId); + if (!newGLAccountId.equals(existingChargeOffReasonToGLAccountMapping.getGlAccount().getId())) { + final Optional glAccount = accountRepository.findById(newGLAccountId); + if (glAccount.isPresent()) { + existingChargeOffReasonToGLAccountMapping.setGlAccount(glAccount.get()); + this.accountMappingRepository.saveAndFlush(existingChargeOffReasonToGLAccountMapping); + } + } + } // deleted payment type + else { + this.accountMappingRepository.delete(existingChargeOffReasonToGLAccountMapping); + } + } + } + + // only the newly added + for (Map.Entry entry : inputChargeOffReasonToGLAccountMap.entrySet().stream() + .filter(e -> !existingChargeOffReasons.contains(e.getKey())).toList()) { + saveChargeOffReasonToExpenseMapping(productId, entry.getKey(), entry.getValue(), portfolioProductType); + } + } + } + } + /** * @param productId * @@ -402,6 +489,24 @@ private void saveChargeToFundSourceMapping(final Long productId, final Long char this.accountMappingRepository.saveAndFlush(accountMapping); } + private void saveChargeOffReasonToExpenseMapping(final Long productId, final Long reasonId, final Long expenseAccountId, + final PortfolioProductType portfolioProductType) { + + final Optional glAccount = accountRepository.findById(expenseAccountId); + + final boolean reasonMappingExists = this.accountMappingRepository + .findAllChargesOffReasonsMappings(productId, portfolioProductType.getValue()).stream() + .anyMatch(mapping -> mapping.getChargeOffReasonId().equals(reasonId)); + + if (glAccount.isPresent() && !reasonMappingExists) { + final ProductToGLAccountMapping accountMapping = new ProductToGLAccountMapping().setGlAccount(glAccount.get()) + .setProductId(productId).setProductType(portfolioProductType.getValue()) + .setFinancialAccountType(CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue()).setChargeOffReasonId(reasonId); + + this.accountMappingRepository.saveAndFlush(accountMapping); + } + } + private List getAllowedAccountTypesForFeeMapping() { List allowedAccountTypes = new ArrayList<>(); allowedAccountTypes.add(GLAccountType.INCOME); @@ -455,4 +560,38 @@ public void deleteProductToGLAccountMapping(final Long loanProductId, final Port this.accountMappingRepository.deleteAllInBatch(productToGLAccountMappings); } } + + public void validateChargeOffMappingsInDatabase(final List mappings) { + final List validationErrors = new ArrayList<>(); + + for (JsonObject jsonObject : mappings) { + final Long expenseGlAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject); + final Long chargeOffReasonCodeValueId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), jsonObject); + + // Validation: chargeOffReasonCodeValueId must exist in the database + CodeValue codeValue = this.codeValueRepository.findByCodeNameAndId("ChargeOffReasons", chargeOffReasonCodeValueId); + if (codeValue == null) { + validationErrors.add(ApiParameterError.parameterError("validation.msg.chargeoffreason.invalid", + "Charge-off reason with ID " + chargeOffReasonCodeValueId + " does not exist", + LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue())); + } + + // Validation: expenseGLAccountId must exist as a valid Expense GL account + final Optional glAccount = accountRepository.findById(expenseGlAccountId); + + if (glAccount.isEmpty() || !glAccount.get().getType().equals(GL_ACCOUNT_EXPENSE_TYPE)) { + validationErrors.add(ApiParameterError.parameterError("validation.msg.glaccount.not.found", + "GL Account with ID " + expenseGlAccountId + " does not exist or is not an Expense GL account", + LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue())); + + } + } + + // Throw all collected validation errors, if any + if (!validationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(validationErrors); + } + } } diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java index 47df148a8eb..612c23c1244 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java @@ -46,5 +46,4 @@ public interface ProductToGLAccountMappingReadPlatformService { List fetchPaymentTypeToFundSourceMappingsForShareProduct(Long productId); List fetchFeeToIncomeAccountMappingsForShareProduct(Long productId); - } diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java index 18b7da24ebf..903ae202546 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/SavingsProductToGLAccountMappingHelper.java @@ -29,6 +29,7 @@ import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper; import org.apache.fineract.accounting.glaccount.domain.GLAccountType; import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; +import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; @@ -42,9 +43,9 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa public SavingsProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper, - final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper) { + final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper, final CodeValueRepository codeValueRepository) { super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper, - paymentTypeRepositoryWrapper); + paymentTypeRepositoryWrapper, codeValueRepository); } /*** diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ShareProductToGLAccountMappingHelper.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ShareProductToGLAccountMappingHelper.java index b026b428bc6..b7314f693ac 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ShareProductToGLAccountMappingHelper.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ShareProductToGLAccountMappingHelper.java @@ -28,6 +28,7 @@ import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper; import org.apache.fineract.accounting.glaccount.domain.GLAccountType; import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; +import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; @@ -41,9 +42,9 @@ public class ShareProductToGLAccountMappingHelper extends ProductToGLAccountMapp public ShareProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper, - final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper) { + final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper, final CodeValueRepository codeValueRepository) { super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper, - paymentTypeRepositoryWrapper); + paymentTypeRepositoryWrapper, codeValueRepository); } /*** diff --git a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java index 669ad59dcbb..ce7a2ff9fb4 100644 --- a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java @@ -172,7 +172,10 @@ public enum LoanProductAccountingParams { INCOME_FROM_CHARGE_OFF_PENALTY("incomeFromChargeOffPenaltyAccountId"), // INCOME_FROM_GOODWILL_CREDIT_INTEREST("incomeFromGoodwillCreditInterestAccountId"), // INCOME_FROM_GOODWILL_CREDIT_FEES("incomeFromGoodwillCreditFeesAccountId"), // - INCOME_FROM_GOODWILL_CREDIT_PENALTY("incomeFromGoodwillCreditPenaltyAccountId"); // + INCOME_FROM_GOODWILL_CREDIT_PENALTY("incomeFromGoodwillCreditPenaltyAccountId"), // + CHARGE_OFF_REASONS_TO_EXPENSE("chargeOffReasonsToExpenseMappings"), // + EXPENSE_GL_ACCOUNT_ID("expenseGLAccountId"), // + CHARGE_OFF_REASON_CODE_VALUE_ID("chargeOffReasonCodeValueId"); // private final String value; diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java index f78119b94ec..c0b64833688 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java @@ -288,6 +288,9 @@ public static class FineractExternalEventsProperties { private boolean enabled; private FineractExternalEventsProducerProperties producer; private int partitionSize; + private int threadPoolCorePoolSize; + private int threadPoolMaxPoolSize; + private int threadPoolQueueCapacity; } @Getter diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/config/EventTaskExecutorConfig.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/config/EventTaskExecutorConfig.java new file mode 100644 index 00000000000..84322bb4511 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/config/EventTaskExecutorConfig.java @@ -0,0 +1,44 @@ +/** + * 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.infrastructure.event.external.config; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.config.FineractProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +@RequiredArgsConstructor +public class EventTaskExecutorConfig { + + private final FineractProperties fineractProperties; + + @Bean(TaskExecutorConstant.EVENT_MARKS_AS_SENT_EXECUTOR_BEAN_NAME) + public ThreadPoolTaskExecutor sendAsynchronousEventsThreadPool() { + ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); + threadPoolTaskExecutor.setCorePoolSize(fineractProperties.getEvents().getExternal().getThreadPoolCorePoolSize()); + threadPoolTaskExecutor.setMaxPoolSize(fineractProperties.getEvents().getExternal().getThreadPoolMaxPoolSize()); + threadPoolTaskExecutor.setQueueCapacity(fineractProperties.getEvents().getExternal().getThreadPoolQueueCapacity()); + threadPoolTaskExecutor.setThreadNamePrefix("external-events-"); + threadPoolTaskExecutor.initialize(); + + return threadPoolTaskExecutor; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/config/TaskExecutorConstant.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/config/TaskExecutorConstant.java new file mode 100644 index 00000000000..b7c3ef868c0 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/config/TaskExecutorConstant.java @@ -0,0 +1,26 @@ +/** + * 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.infrastructure.event.external.config; + +public final class TaskExecutorConstant { + + private TaskExecutorConstant() {} + + public static final String EVENT_MARKS_AS_SENT_EXECUTOR_BEAN_NAME = "eventMarksAsSentExecutor"; +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/jobs/SendAsynchronousEventsTasklet.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/jobs/SendAsynchronousEventsTasklet.java index 51ef86d69ac..70b514ee891 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/jobs/SendAsynchronousEventsTasklet.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/jobs/SendAsynchronousEventsTasklet.java @@ -29,12 +29,17 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.avro.MessageV1; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.config.FineractProperties; +import org.apache.fineract.infrastructure.core.domain.FineractContext; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.event.external.config.TaskExecutorConstant; import org.apache.fineract.infrastructure.event.external.producer.ExternalEventProducer; import org.apache.fineract.infrastructure.event.external.repository.ExternalEventRepository; import org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventStatus; @@ -45,9 +50,12 @@ import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionTemplate; @Slf4j @RequiredArgsConstructor @@ -60,6 +68,9 @@ public class SendAsynchronousEventsTasklet implements Tasklet { private final MessageFactory messageFactory; private final ByteBufferConverter byteBufferConverter; private final ConfigurationDomainService configurationDomainService; + private final TransactionTemplate transactionTemplate; + @Qualifier(TaskExecutorConstant.EVENT_MARKS_AS_SENT_EXECUTOR_BEAN_NAME) + private final ThreadPoolTaskExecutor threadPoolTaskExecutor; @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { @@ -83,7 +94,7 @@ private boolean isDownstreamChannelEnabled() { private List getQueuedEventsBatch() { int readBatchSize = getBatchSize(); Pageable batchSize = PageRequest.ofSize(readBatchSize); - return measure(() -> repository.findByStatusOrderById(ExternalEventStatus.TO_BE_SENT, batchSize), + return measure(() -> repository.findByStatusOrderByBusinessDateAscIdAsc(ExternalEventStatus.TO_BE_SENT, batchSize), (events, timeTaken) -> log.debug("Loaded {} events in {}ms", events.size(), timeTaken.toMillis())); } @@ -104,14 +115,31 @@ private void markEventsAsSent(List eventIds) { // Partitioning dataset to avoid exception: PreparedStatement can have at most 65,535 parameters final int partitionSize = fineractProperties.getEvents().getExternal().getPartitionSize(); List> partitions = Lists.partition(eventIds, partitionSize); + List> tasks = new ArrayList<>(); + final FineractContext context = ThreadLocalContextUtil.getContext(); partitions // .forEach(partitionedEventIds -> { - measure(() -> { - repository.markEventsSent(partitionedEventIds, sentAt); - }, timeTaken -> { - log.debug("Took {}ms to update {} events", timeTaken.toMillis(), partitionedEventIds.size()); - }); + tasks.add(threadPoolTaskExecutor.submit(() -> { + ThreadLocalContextUtil.init(context); + transactionTemplate.execute((status) -> { + measure(() -> { + repository.markEventsSent(partitionedEventIds, sentAt); + }, timeTaken -> { + log.debug("Took {}ms to update {} events", timeTaken.toMillis(), partitionedEventIds.size()); + }); + return null; + }); + })); }); + for (Future task : tasks) { + try { + task.get(); + } catch (InterruptedException e) { + log.error("Interrupted while marking events as sent", e); + } catch (ExecutionException e) { + log.error("Exception while marking events as sent", e); + } + } } private Map> generatePartitions(List queuedEvents) { diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java index 9f7ccbddce8..aff28cc7416 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java @@ -33,7 +33,7 @@ public interface ExternalEventRepository extends JpaRepository, JpaSpecificationExecutor { - List findByStatusOrderById(ExternalEventStatus status, Pageable batchSize); + List findByStatusOrderByBusinessDateAscIdAsc(ExternalEventStatus status, Pageable batchSize); @Modifying(flushAutomatically = true) @Query("delete from ExternalEvent e where e.status = :status and e.businessDate <= :dateForPurgeCriteria") diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java index c157a5486db..e70974cc6ad 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java @@ -198,6 +198,14 @@ public Money copy() { return new Money(this.currencyCode, this.currencyDigitsAfterDecimal, this.amount.stripTrailingZeros(), this.inMultiplesOf, this.mc); } + public Money copy(final BigDecimal amount) { + return new Money(this.currencyCode, this.currencyDigitsAfterDecimal, amount.stripTrailingZeros(), this.inMultiplesOf, this.mc); + } + + public Money copy(final double amount) { + return copy(BigDecimal.valueOf(amount)); + } + public Money plus(final Iterable moniesToAdd) { BigDecimal total = this.amount; for (final Money moneyProvider : moniesToAdd) { @@ -313,6 +321,14 @@ public Money dividedBy(final long valueToDivideBy, final MathContext mc) { return Money.of(monetaryCurrency(), newAmount, mc); } + public Money dividedBy(final long valueToDivideBy) { + if (valueToDivideBy == 1) { + return this; + } + final BigDecimal newAmount = this.amount.divide(BigDecimal.valueOf(valueToDivideBy), getMc()); + return Money.of(monetaryCurrency(), newAmount, getMc()); + } + public Money multipliedBy(final BigDecimal valueToMultiplyBy) { return multipliedBy(valueToMultiplyBy, getMc()); } diff --git a/fineract-e2e-tests-core/build.gradle b/fineract-e2e-tests-core/build.gradle index e1b03644ca9..14ccb5ff380 100644 --- a/fineract-e2e-tests-core/build.gradle +++ b/fineract-e2e-tests-core/build.gradle @@ -51,16 +51,16 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.3' testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' - testCompileOnly 'org.projectlombok:lombok:1.18.34' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' + testCompileOnly 'org.projectlombok:lombok:1.18.36' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' testImplementation "ch.qos.logback:logback-core:1.5.12" testImplementation "ch.qos.logback:logback-classic:1.5.12" - testImplementation 'org.apache.activemq:activemq-client:6.1.3' + testImplementation 'org.apache.activemq:activemq-client:6.1.4' testImplementation "org.apache.avro:avro:1.12.0" testImplementation "org.awaitility:awaitility:4.2.2" - testImplementation 'io.github.classgraph:classgraph:4.8.177' + testImplementation 'io.github.classgraph:classgraph:4.8.179' testImplementation 'org.apache.commons:commons-collections4:4.4' } diff --git a/fineract-e2e-tests-runner/build.gradle b/fineract-e2e-tests-runner/build.gradle index 1fb7143dba2..d8da800854e 100644 --- a/fineract-e2e-tests-runner/build.gradle +++ b/fineract-e2e-tests-runner/build.gradle @@ -54,16 +54,16 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.3' testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3' - testCompileOnly 'org.projectlombok:lombok:1.18.34' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' + testCompileOnly 'org.projectlombok:lombok:1.18.36' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' testImplementation "ch.qos.logback:logback-core:1.5.12" testImplementation "ch.qos.logback:logback-classic:1.5.12" - testImplementation 'org.apache.activemq:activemq-client:6.1.3' + testImplementation 'org.apache.activemq:activemq-client:6.1.4' testImplementation "org.apache.avro:avro:1.12.0" testImplementation "org.awaitility:awaitility:4.2.2" - testImplementation 'io.github.classgraph:classgraph:4.8.177' + testImplementation 'io.github.classgraph:classgraph:4.8.179' testImplementation 'org.apache.commons:commons-collections4:4.4' } diff --git a/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java b/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java index b40a343e0f0..46140441985 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java @@ -33,6 +33,7 @@ import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingInvalidException; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingHelper; +import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; @@ -46,9 +47,9 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi public LoanProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper, - final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper) { + final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper, final CodeValueRepository codeValueRepository) { super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper, - paymentTypeRepositoryWrapper); + paymentTypeRepositoryWrapper, codeValueRepository); } /*** @@ -138,6 +139,16 @@ public void saveChargesToIncomeAccountMappings(final JsonCommand command, final saveChargesToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, false); } + public void saveChargeOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, + final Map changes) { + saveChargeOffReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN); + } + + public void updateChargeOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, + final Map changes) { + updateChargeOffReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN); + } + public void updateChargesToIncomeAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, final Map changes) { // update both fee and penalty charges @@ -387,5 +398,4 @@ private GLAccountType getGLAccountType(final JsonElement element, final String p } return gLAccountType; } - } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 0c0f3e62dcb..73198f30907 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -2438,7 +2438,8 @@ public ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(fin } } if (reprocess) { - if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + if (this.repaymentScheduleDetail().isInterestRecalculationEnabled() + && !getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)) { regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); } reprocessTransactions(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java index ae0c48505ad..d12449b894b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java @@ -46,7 +46,7 @@ public void reprocess(final MonetaryCurrency currency, final LocalDate disbursem totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); } List accruals = null; - if (!loan.isInterestBearing() && loanCharge.isSpecifiedDueDate()) { // TODO: why only if not interest bearing + if (loanCharge.isSpecifiedDueDate()) { LoanRepaymentScheduleInstallment addedPeriod = addChargeOnlyRepaymentInstallmentIfRequired(loanCharge, installments); if (addedPeriod != null) { addedPeriod.updateObligationsMet(currency, disbursementDate); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java index 5ecfb6cff49..e83786baaf1 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java @@ -246,6 +246,7 @@ private PostLoanProductsRequest() {} public Long incomeFromGoodwillCreditPenaltyAccountId; public List paymentChannelToFundSourceMappings; public List feeToIncomeAccountMappings; + public List chargeOffReasonsToExpenseMappings; public List penaltyToIncomeAccountMappings; // Multi Disburse @@ -1209,6 +1210,16 @@ private GetLoanPaymentChannelToFundSourceMappings() {} public Long fundSourceAccountId; } + static final class GetChargeOffReasonsToExpenseMappings { + + private GetChargeOffReasonsToExpenseMappings() {} + + @Schema(example = "1") + public Long chargeOffReasonCodeValueId; + @Schema(example = "12") + public Long expenseGLAccountId; + } + static final class GetLoanFeeToIncomeAccountMappings { private GetLoanFeeToIncomeAccountMappings() {} @@ -1319,6 +1330,7 @@ private GetLoanCharge() {} public GetLoanAccountingMappings accountingMappings; public Set paymentChannelToFundSourceMappings; public Set feeToIncomeAccountMappings; + public Set chargeOffReasonsToExpenseMappings; @Schema(example = "false") public Boolean isRatesEnabled; @Schema(example = "true") @@ -1577,6 +1589,7 @@ private PutLoanProductsProductIdRequest() {} public Long incomeFromChargeOffPenaltyAccountId; public List paymentChannelToFundSourceMappings; public List feeToIncomeAccountMappings; + public List chargeOffReasonsToExpenseMappings; public List penaltyToIncomeAccountMappings; @Schema(example = "false") public Boolean enableAccrualActivityPosting; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java index c32997cb667..dd1273338c5 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java @@ -151,6 +151,7 @@ public class LoanProductData implements Serializable { private Collection paymentChannelToFundSourceMappings; private Collection feeToIncomeAccountMappings; private Collection penaltyToIncomeAccountMappings; + private List chargeOffReasonsToExpenseMappings; private final boolean enableAccrualActivityPosting; // rates @@ -853,6 +854,7 @@ public LoanProductData(final Long id, final String name, final String shortName, this.paymentChannelToFundSourceMappings = null; this.feeToIncomeAccountMappings = null; this.penaltyToIncomeAccountMappings = null; + this.chargeOffReasonsToExpenseMappings = null; this.valueConditionTypeOptions = null; this.principalVariationsForBorrowerCycle = principalVariations; this.interestRateVariationsForBorrowerCycle = interestRateVariations; @@ -992,6 +994,7 @@ public LoanProductData(final LoanProductData productData, final Collection relatedRepaymentPeriods, // + long uncountablePeriods// +) { + + public boolean shouldBeAdjusted() { + double lowerHalfOfRelatedPeriods = Math.floor(numberOfRelatedPeriods() / 2.0); + return lowerHalfOfRelatedPeriods > 0.0 && !emiDifference.isZero() && emiDifference.abs() // + .multipliedBy(100) // + .isGreaterThan(originalEmi.copy(lowerHalfOfRelatedPeriods)); // + } + + public Money adjustment() { + return emiDifference.dividedBy(Math.max(1, numberOfRelatedPeriods() - uncountablePeriods)); + } + + public Money adjustedEmi() { + return originalEmi.plus(adjustment()); + } + + public boolean hasLessEmiDifference(EmiAdjustment previousAdjustment) { + return emiDifference.abs().isLessThan(previousAdjustment.emiDifference.abs()); + } + + public boolean hasUncountablePeriods() { + return uncountablePeriods > 0; + } + + private int numberOfRelatedPeriods() { + return relatedRepaymentPeriods.size(); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index f3aa14ec7f3..61689705996 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -36,6 +36,7 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.EmiAdjustment; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails; @@ -311,33 +312,16 @@ private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final Progressiv final List relatedRepaymentPeriods) { MathContext mc = scheduleModel.mc(); ProgressiveLoanInterestScheduleModel newScheduleModel = null; - - final int numberOfRelatedPeriods = relatedRepaymentPeriods.size(); - double lowerHalfOfRelatedPeriods = Math.floor(numberOfRelatedPeriods / 2.0); - if (lowerHalfOfRelatedPeriods == 0.0) { - return; - } - - long uncountablePeriods; int adjustCounter = 0; + EmiAdjustment emiAdjustment; do { - final Money emiDifference = getDifferenceBetweenLastTwoPeriod(relatedRepaymentPeriods, scheduleModel); - if (emiDifference.isZero(mc)) { - break; - } - final Money originalEmi = relatedRepaymentPeriods.get(numberOfRelatedPeriods - 2).getEmi(); - boolean shouldBeAdjusted = emiDifference.abs(mc).multipliedBy(100, mc) - .isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods), mc)); - if (!shouldBeAdjusted) { + emiAdjustment = getEmiAdjustment(relatedRepaymentPeriods); + if (!emiAdjustment.shouldBeAdjusted()) { break; } - - uncountablePeriods = relatedRepaymentPeriods.stream().filter(rp -> originalEmi.isLessThan(rp.getTotalPaidAmount())).count(); - Money adjustment = emiDifference.dividedBy(Math.max(1, numberOfRelatedPeriods - uncountablePeriods), mc); - Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel, - originalEmi.plus(adjustment, mc)); - if (adjustedEqualMonthlyInstallmentValue.isEqualTo(originalEmi)) { + Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel, emiAdjustment.adjustedEmi()); + if (adjustedEqualMonthlyInstallmentValue.isEqualTo(emiAdjustment.originalEmi())) { break; } if (newScheduleModel == null) { @@ -353,9 +337,7 @@ private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final Progressiv }); calculateOutstandingBalance(newScheduleModel); calculateLastUnpaidRepaymentPeriodEMI(newScheduleModel); - final Money newEmiDifference = getDifferenceBetweenLastTwoPeriod(newScheduleModel.repaymentPeriods(), scheduleModel); - final boolean newEmiHasLessDifference = newEmiDifference.abs(mc).isLessThan(emiDifference.abs(mc)); - if (!newEmiHasLessDifference) { + if (!getEmiAdjustment(newScheduleModel.repaymentPeriods()).hasLessEmiDifference(emiAdjustment)) { break; } @@ -373,7 +355,7 @@ private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final Progressiv }); calculateOutstandingBalance(scheduleModel); adjustCounter++; - } while (uncountablePeriods > 0 && adjustCounter < 3); + } while (emiAdjustment.hasUncountablePeriods() && adjustCounter < 3); } /** @@ -524,16 +506,17 @@ Money applyInstallmentAmountInMultiplesOf(final ProgressiveLoanInterestScheduleM : equalMonthlyInstallment; } - Money getDifferenceBetweenLastTwoPeriod(final List repaymentPeriods, - final ProgressiveLoanInterestScheduleModel scheduleModel) { - MathContext mc = scheduleModel.mc(); - int numberOfUpcomingPeriods = repaymentPeriods.size(); - if (numberOfUpcomingPeriods < 2) { - return Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency(), mc); + public EmiAdjustment getEmiAdjustment(final List repaymentPeriods) { + for (int idx = repaymentPeriods.size() - 1; idx > 0; --idx) { + RepaymentPeriod lastPeriod = repaymentPeriods.get(idx); + RepaymentPeriod penultimatePeriod = repaymentPeriods.get(idx - 1); + if (!lastPeriod.isFullyPaid() && !penultimatePeriod.isFullyPaid()) { + Money emiDifference = lastPeriod.getEmi().minus(penultimatePeriod.getEmi()); + return new EmiAdjustment(penultimatePeriod.getEmi(), emiDifference, repaymentPeriods, + getUncountablePeriods(repaymentPeriods, penultimatePeriod.getEmi())); + } } - final RepaymentPeriod lastPeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 1); - final RepaymentPeriod penultimatePeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 2); - return lastPeriod.getEmi().minus(penultimatePeriod.getEmi(), mc); + return new EmiAdjustment(repaymentPeriods.get(0).getEmi(), repaymentPeriods.get(0).getEmi().copy(0.0), repaymentPeriods, 0); } /** @@ -742,4 +725,10 @@ public Money getSumOfDueInterestsOnDate(ProgressiveLoanInterestScheduleModel sch .getDueInterest()) // .reduce(scheduleModel.getZero(), Money::add); // } + + private long getUncountablePeriods(final List relatedRepaymentPeriods, final Money originalEmi) { + return relatedRepaymentPeriods.stream() // + .filter(repaymentPeriod -> originalEmi.isLessThan(repaymentPeriod.getTotalPaidAmount())) // + .count(); // + } } diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index 22e3d4baa2c..b63b3d2fea4 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -472,6 +472,53 @@ public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt1 checkPeriod(interestModel, 5, 0, 16.85, 0.003333333333, 0.06, 16.79, 0.0); } + @Test + public void test_reschedule_interest_on0120_adjsLst_dsbAmt100_dayInYears360_daysInMonth30_rpEvery1M() { + + final List expectedRepaymentPeriods = new ArrayList<>(); + + expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); + expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1))); + expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1))); + expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1))); + expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); + expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); + + final BigDecimal interestRate = BigDecimal.valueOf(7.0); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); + + threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); + + final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, + loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(100.0); + emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount); + + emiCalculator.payPrincipal(interestModel, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 1, 15), toMoney(17.01)); + + final BigDecimal interestRateNewValue = BigDecimal.valueOf(4.0); + final LocalDate interestChangeDate = LocalDate.of(2024, 1, 20); + emiCalculator.changeInterestRate(interestModel, interestChangeDate, interestRateNewValue); + + checkPeriod(interestModel, 0, 0, 16.80, 0.0, 0.0, 0.44, 16.36, 66.63); + checkPeriod(interestModel, 0, 1, 16.80, 0.002634408602, 0.26, 0.44, 16.36, 66.63); + checkPeriod(interestModel, 0, 2, 16.80, 0.000752688172, 0.06, 0.44, 16.36, 66.63); + checkPeriod(interestModel, 0, 3, 16.80, 0.001397849462, 0.12, 0.44, 16.36, 66.63); + checkPeriod(interestModel, 1, 0, 16.80, 0.003333333333, 0.22, 16.58, 50.05); + checkPeriod(interestModel, 2, 0, 16.80, 0.003333333333, 0.17, 16.63, 33.42); + checkPeriod(interestModel, 3, 0, 16.80, 0.003333333333, 0.11, 16.69, 16.73); + checkPeriod(interestModel, 4, 0, 16.79, 0.003333333333, 0.06, 16.73, 0.0); + checkPeriod(interestModel, 5, 0, 17.01, 0.003333333333, 0.0, 17.01, 0.0); + } + @Test public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); diff --git a/fineract-provider/config/swagger/fineract-input.yaml.template b/fineract-provider/config/swagger/fineract-input.yaml.template index f7abfe93cc8..39494291bae 100644 --- a/fineract-provider/config/swagger/fineract-input.yaml.template +++ b/fineract-provider/config/swagger/fineract-input.yaml.template @@ -14,8 +14,8 @@ info: name: Apache 2.0 url: 'http://www.apache.org/licenses/LICENSE-2.0.html' servers: - - url: /fineract-provider/api/v1 - - url: https://demo.fineract.dev/fineract-provider/api/v1 + - url: /fineract-provider/api + - url: https://demo.fineract.dev/fineract-provider/api components: securitySchemes: basicAuth: diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java index 53496003a55..05d8a530c91 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java @@ -130,6 +130,7 @@ public void createLoanProductToGLAccountMapping(final Long loanProductId, final // advanced accounting mappings this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null); + this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null); break; case ACCRUAL_UPFRONT: // Fall Through @@ -208,6 +209,7 @@ public void createLoanProductToGLAccountMapping(final Long loanProductId, final // advanced accounting mappings this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null); + this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null); break; } } @@ -379,6 +381,8 @@ public Map updateLoanProductToGLAccountMapping(final Long loanPr accountingRuleType); this.loanProductToGLAccountMappingHelper.updatePaymentChannelToFundSourceMappings(command, element, loanProductId, changes); this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes); + this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, + changes); } return changes; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index 7adcbdefd8d..b2ac2594297 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -160,7 +160,7 @@ public class LoanProductsApiResource { @Operation(summary = "Create a Loan Product", description = "Depending of the Accounting Rule (accountingRule) selected, additional fields with details of the appropriate Ledger Account identifiers would need to be passed in.\n" + "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n" + "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n" - + "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n" + + "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, chargeOffReasonsToExpenseMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n" + "Additional Mandatory Fields for Cash(2) based accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n" + "Additional Mandatory Fields for periodic (3) and upfront (4)accrual accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n" + "Additional Mandatory Fields if interest recalculation is enabled(true): interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType\n\n" @@ -336,6 +336,7 @@ private String getLoanProductDetails(Long productId, UriInfo uriInfo) { Collection paymentChannelToFundSourceMappings; Collection feeToGLAccountMappings; Collection penaltyToGLAccountMappings; + List chargeOffReasonsToExpenseMappings; if (loanProduct.hasAccountingEnabled()) { accountingMappings = this.accountMappingReadPlatformService.fetchAccountMappingDetailsForLoanProduct(productId, loanProduct.getAccountingRule().getId().intValue()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 3758e4e20ab..e0d45318ed1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -36,6 +37,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; import org.apache.fineract.accounting.common.AccountingValidations; +import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingHelper; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; @@ -141,7 +143,9 @@ public final class LoanProductDataValidator { LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), - LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME, + LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(), + LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), + LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.PRINCIPAL_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.INTEREST_RATE_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.NUMBER_OF_REPAYMENT_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.SHORT_NAME, @@ -195,6 +199,7 @@ public final class LoanProductDataValidator { private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; private final AdvancedPaymentAllocationsJsonParser advancedPaymentAllocationsJsonParser; private final AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator; + private final ProductToGLAccountMappingHelper productToGLAccountMappingHelper; public void validateForCreate(final JsonCommand command) { String json = command.json(); @@ -718,6 +723,7 @@ public void validateForCreate(final JsonCommand command) { validatePaymentChannelFundSourceMappings(baseDataValidator, element); validateChargeToIncomeAccountMappings(baseDataValidator, element); + validateChargeOffToExpenseMappings(baseDataValidator, element); } @@ -1791,6 +1797,7 @@ public void validateForUpdate(final JsonCommand command, final LoanProduct loanP validatePaymentChannelFundSourceMappings(baseDataValidator, element); validateChargeToIncomeAccountMappings(baseDataValidator, element); + validateChargeOffToExpenseMappings(baseDataValidator, element); validateMinMaxConstraints(element, baseDataValidator, loanProduct); @@ -1964,6 +1971,60 @@ private void validateChargeToIncomeAccountMappings(final DataValidatorBuilder ba } } + private void validateChargeOffToExpenseMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) { + String parameterName = LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(); + + if (this.fromApiJsonHelper.parameterExists(parameterName, element)) { + final JsonArray chargeOffToExpenseMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element); + if (chargeOffToExpenseMappingArray != null && chargeOffToExpenseMappingArray.size() > 0) { + Map> chargeOffReasonToAccounts = new HashMap<>(); + List processedMappings = new ArrayList<>(); // Collect processed mappings for the new method + + int i = 0; + do { + final JsonObject jsonObject = chargeOffToExpenseMappingArray.get(i).getAsJsonObject(); + final Long expenseGlAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject); + final Long chargeOffReasonCodeValueId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), jsonObject); + + // Validate parameters locally + baseDataValidator.reset() + .parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT + + LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()) + .value(expenseGlAccountId).notNull().integerGreaterThanZero(); + baseDataValidator.reset() + .parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT + + LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue()) + .value(chargeOffReasonCodeValueId).notNull().integerGreaterThanZero(); + + // Handle duplicate charge-off reason and GL Account validation + chargeOffReasonToAccounts.putIfAbsent(chargeOffReasonCodeValueId, new HashSet<>()); + Set associatedAccounts = chargeOffReasonToAccounts.get(chargeOffReasonCodeValueId); + + if (associatedAccounts.contains(expenseGlAccountId)) { + baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET) + .failWithCode("duplicate.chargeOffReason.and.glAccount"); + } + associatedAccounts.add(expenseGlAccountId); + + if (associatedAccounts.size() > 1) { + baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET) + .failWithCode("multiple.glAccounts.for.chargeOffReason"); + } + + // Collect mapping for additional validations + processedMappings.add(jsonObject); + + i++; + } while (i < chargeOffToExpenseMappingArray.size()); + + // Call the new validation method for additional checks + productToGLAccountMappingHelper.validateChargeOffMappingsInDatabase(processedMappings); + } + } + } + public void validateMinMaxConstraints(final JsonElement element, final DataValidatorBuilder baseDataValidator, final LoanProduct loanProduct) { validatePrincipalMinMaxConstraint(element, loanProduct, baseDataValidator); diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 326c3f54b3f..877b05ba9d8 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -100,6 +100,9 @@ fineract.remote-job-message-handler.kafka.admin.extra-properties=${FINERACT_REMO fineract.events.external.enabled=${FINERACT_EXTERNAL_EVENTS_ENABLED:false} fineract.events.external.partition-size=${FINERACT_EXTERNAL_EVENTS_PARTITION_SIZE:5000} +fineract.events.external.thread-pool-core-pool-size=${FINERACT_EVENT_TASK_EXECUTOR_CORE_POOL_SIZE:2} +fineract.events.external.thread-pool-max-pool-size=${FINERACT_EVENT_TASK_EXECUTOR_MAX_POOL_SIZE:25} +fineract.events.external.thread-pool-queue-capacity=${FINERACT_EVENT_TASK_EXECUTOR_QUEUE_CAPACITY:500} fineract.events.external.producer.jms.enabled=${FINERACT_EXTERNAL_EVENTS_PRODUCER_JMS_ENABLED:false} fineract.events.external.producer.jms.async-send-enabled=${FINERACT_EXTERNAL_EVENTS_PRODUCER_JMS_ASYNC_SEND_ENABLED:false} fineract.events.external.producer.jms.event-queue-name=${FINERACT_EXTERNAL_EVENTS_PRODUCER_JMS_QUEUE_NAME:} 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 7b79a6754f1..2a0d2d0c32e 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 @@ -171,4 +171,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0153_add_charge_off_reason_id_to_acc_product_mapping.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0153_add_charge_off_reason_id_to_acc_product_mapping.xml new file mode 100644 index 00000000000..5002a38bbc0 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0153_add_charge_off_reason_id_to_acc_product_mapping.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/jobs/SendAsynchronousEventsTaskletTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/jobs/SendAsynchronousEventsTaskletTest.java index 0caaf3c4e9c..0c71e34b4f9 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/jobs/SendAsynchronousEventsTaskletTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/jobs/SendAsynchronousEventsTaskletTest.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Map; import java.util.Random; +import java.util.concurrent.TimeUnit; import org.apache.fineract.avro.MessageV1; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; @@ -47,11 +48,13 @@ import org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventView; import org.apache.fineract.infrastructure.event.external.service.message.MessageFactory; import org.apache.fineract.infrastructure.event.external.service.support.ByteBufferConverter; +import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -61,6 +64,10 @@ import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -82,6 +89,10 @@ class SendAsynchronousEventsTaskletTest { private ByteBufferConverter byteBufferConverter; @Mock private ConfigurationDomainService configurationDomainService; + @Mock + private TransactionTemplate transactionTemplate; + @Mock + private TransactionStatus transactionStatus; private SendAsynchronousEventsTasklet underTest; private RepeatStatus resultStatus; @@ -94,8 +105,13 @@ public void setUp() { ThreadLocalContextUtil .setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault())))); configureExternalEventsProducerReadBatchSizeProperty(); + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setMaxPoolSize(2); + taskExecutor.initialize(); + when(transactionTemplate.execute(ArgumentMatchers.any())) + .thenAnswer(invocation -> invocation.>getArgument(0).doInTransaction(transactionStatus)); underTest = new SendAsynchronousEventsTasklet(fineractProperties, repository, eventProducer, messageFactory, byteBufferConverter, - configurationDomainService); + configurationDomainService, transactionTemplate, taskExecutor); } @AfterEach @@ -113,6 +129,9 @@ private void configureExternalEventsProducerReadBatchSizeProperty() { externalProperties.setPartitionSize(5000); externalEventsProducerProperties.setJms(externalEventsProducerJMSProperties); externalProperties.setProducer(externalEventsProducerProperties); + externalProperties.setThreadPoolCorePoolSize(1); + externalProperties.setThreadPoolMaxPoolSize(1); + externalProperties.setThreadPoolQueueCapacity(10); eventsProperties.setExternal(externalProperties); when(fineractProperties.getEvents()).thenReturn(eventsProperties); when(configurationDomainService.retrieveExternalEventBatchSize()).thenReturn(10L); @@ -128,14 +147,16 @@ public void givenBatchSize2WhenTaskExecutionThenSend2Events() throws Exception { MessageV1 dummyMessage = new MessageV1(1, "aSource", "aType", "nocategory", "aCreateDate", "aBusinessDate", "aTenantId", "anidempotencyKey", "aSchema", Mockito.mock(ByteBuffer.class)); - when(repository.findByStatusOrderById(Mockito.any(), Mockito.any())).thenReturn(events); + when(repository.findByStatusOrderByBusinessDateAscIdAsc(Mockito.any(), Mockito.any())).thenReturn(events); when(messageFactory.createMessage(Mockito.any())).thenReturn(dummyMessage); when(byteBufferConverter.convert(Mockito.any(ByteBuffer.class))).thenReturn(new byte[0]); // when resultStatus = underTest.execute(stepContribution, chunkContext); // then verify(eventProducer).sendEvents(Mockito.any()); - verify(repository).markEventsSent(Mockito.eq(events.stream().map(ExternalEventView::getId).toList()), Mockito.any()); + Awaitility.await().atMost(10L, TimeUnit.SECONDS).untilAsserted(() -> { + verify(repository).markEventsSent(Mockito.eq(events.stream().map(ExternalEventView::getId).toList()), Mockito.any()); + }); assertEquals(RepeatStatus.FINISHED, resultStatus); } @@ -147,7 +168,7 @@ public void givenBatchSize2WhenEventSendFailsThenExecutionStops() throws Excepti createExternalEventView("aType", "aCategory", "aSchema", new byte[0], "aIdempotencyKey", 1L)); MessageV1 dummyMessage = new MessageV1(1, "aSource", "aType", "nocategory", "aCreateDate", "aBusinessDate", "aTenantId", "anidempotencyKey", "aSchema", Mockito.mock(ByteBuffer.class)); - when(repository.findByStatusOrderById(Mockito.any(), Mockito.any())).thenReturn(events); + when(repository.findByStatusOrderByBusinessDateAscIdAsc(Mockito.any(), Mockito.any())).thenReturn(events); when(messageFactory.createMessage(Mockito.any())).thenReturn(dummyMessage); when(byteBufferConverter.convert(Mockito.any(ByteBuffer.class))).thenReturn(new byte[0]); doThrow(new AcknowledgementTimeoutException("Event Send Exception", new RuntimeException())).when(eventProducer) @@ -166,7 +187,7 @@ public void givenOneEventWhenEventSentThenEventStatusUpdates() throws Exception .asList(createExternalEventView("aType", "aCategory", "aSchema", new byte[0], "aIdempotencyKey", 1L)); MessageV1 dummyMessage = new MessageV1(1, "aSource", "aType", "nocategory", "aCreateDate", "aBusinessDate", "aTenantId", "anidempotencyKey", "aSchema", Mockito.mock(ByteBuffer.class)); - when(repository.findByStatusOrderById(Mockito.any(), Mockito.any())).thenReturn(events); + when(repository.findByStatusOrderByBusinessDateAscIdAsc(Mockito.any(), Mockito.any())).thenReturn(events); when(messageFactory.createMessage(Mockito.any())).thenReturn(dummyMessage); when(byteBufferConverter.convert(Mockito.any(ByteBuffer.class))).thenReturn(new byte[0]); // when @@ -174,7 +195,9 @@ public void givenOneEventWhenEventSentThenEventStatusUpdates() throws Exception // then verify(messageFactory).createMessage(Mockito.any()); verify(eventProducer).sendEvents(Mockito.any()); - verify(repository).markEventsSent(Mockito.eq(events.stream().map(ExternalEventView::getId).toList()), Mockito.any()); + Awaitility.await().atMost(10L, TimeUnit.SECONDS).untilAsserted(() -> { + verify(repository).markEventsSent(Mockito.eq(events.stream().map(ExternalEventView::getId).toList()), Mockito.any()); + }); assertEquals(RepeatStatus.FINISHED, resultStatus); } @@ -185,7 +208,7 @@ public void testExecuteShouldHandleNullAggregateId() throws Exception { .asList(createExternalEventView("aType", "aCategory", "aSchema", new byte[0], "aIdempotencyKey", null)); MessageV1 dummyMessage = new MessageV1(1, "aSource", "aType", "nocategory", "aCreateDate", "aBusinessDate", "aTenantId", "anidempotencyKey", "aSchema", Mockito.mock(ByteBuffer.class)); - when(repository.findByStatusOrderById(Mockito.any(), Mockito.any())).thenReturn(events); + when(repository.findByStatusOrderByBusinessDateAscIdAsc(Mockito.any(), Mockito.any())).thenReturn(events); when(messageFactory.createMessage(Mockito.any())).thenReturn(dummyMessage); byte[] byteMsg = new byte[0]; when(byteBufferConverter.convert(Mockito.any(ByteBuffer.class))).thenReturn(byteMsg); @@ -194,7 +217,9 @@ public void testExecuteShouldHandleNullAggregateId() throws Exception { // then verify(messageFactory).createMessage(Mockito.any()); verify(eventProducer).sendEvents(Map.of(-1L, List.of(byteMsg))); - verify(repository).markEventsSent(Mockito.eq(events.stream().map(ExternalEventView::getId).toList()), Mockito.any()); + Awaitility.await().atMost(10L, TimeUnit.SECONDS).untilAsserted(() -> { + verify(repository).markEventsSent(Mockito.eq(events.stream().map(ExternalEventView::getId).toList()), Mockito.any()); + }); assertEquals(RepeatStatus.FINISHED, resultStatus); } @@ -202,11 +227,11 @@ public void testExecuteShouldHandleNullAggregateId() throws Exception { public void givenEventBatchSizeIsConfiguredAs10WhenTaskExecutionThenEventReadPageSizeIsCorrect() { ArgumentCaptor externalEventPageSizeArgumentCaptor = ArgumentCaptor.forClass(Pageable.class); List events = new ArrayList<>(); - when(repository.findByStatusOrderById(Mockito.any(), Mockito.any())).thenReturn(events); + when(repository.findByStatusOrderByBusinessDateAscIdAsc(Mockito.any(), Mockito.any())).thenReturn(events); // when resultStatus = underTest.execute(stepContribution, chunkContext); // then - verify(repository).findByStatusOrderById(Mockito.any(), externalEventPageSizeArgumentCaptor.capture()); + verify(repository).findByStatusOrderByBusinessDateAscIdAsc(Mockito.any(), externalEventPageSizeArgumentCaptor.capture()); assertThat(externalEventPageSizeArgumentCaptor.getValue().getPageSize()).isEqualTo(10); } diff --git a/fineract-provider/src/test/resources/application-test.properties b/fineract-provider/src/test/resources/application-test.properties index fe02f28cf8c..56ee7735625 100644 --- a/fineract-provider/src/test/resources/application-test.properties +++ b/fineract-provider/src/test/resources/application-test.properties @@ -53,6 +53,9 @@ fineract.remote-job-message-handler.jms.enabled=${FINERACT_REMOTE_JOB_MESSAGE_HA fineract.remote-job-message-handler.jms.request-queue-name=${FINERACT_REMOTE_JOB_MESSAGE_HANDLER_JMS_QUEUE_NAME:JMS-request-queue} fineract.events.external.enabled=${FINERACT_EXTERNAL_EVENTS_ENABLED:false} fineract.events.external.partition-size=${FINERACT_EXTERNAL_EVENTS_PARTITION_SIZE:5000} +fineract.events.external.thread-pool-core-pool-size=${FINERACT_EVENT_TASK_EXECUTOR_CORE_POOL_SIZE:2} +fineract.events.external.thread-pool-max-pool-size=${FINERACT_EVENT_TASK_EXECUTOR_MAX_POOL_SIZE:25} +fineract.events.external.thread-pool-queue-capacity=${FINERACT_EVENT_TASK_EXECUTOR_QUEUE_CAPACITY:500} fineract.events.external.producer.read-batch-size=${FINERACT_EXTERNAL_EVENTS_PRODUCER_READ_BATCH_SIZE:1000} fineract.events.external.producer.jms.enabled=${FINERACT_EXTERNAL_EVENTS_PRODUCER_JMS_ENABLED:false} fineract.events.external.producer.jms.event-queue-name=${FINERACT_EXTERNAL_EVENTS_PRODUCER_JMS_QUEUE_NAME:JMS-event-queue} diff --git a/integration-tests/dependencies.gradle b/integration-tests/dependencies.gradle index f153eacf2ff..f8b4e1dbf6d 100644 --- a/integration-tests/dependencies.gradle +++ b/integration-tests/dependencies.gradle @@ -20,7 +20,7 @@ dependencies { // testCompile dependencies are ONLY used in src/test, not src/main. // Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly! // - tomcat 'org.apache.tomcat:tomcat:10.1.31@zip' + tomcat 'org.apache.tomcat:tomcat:10.1.33@zip' testImplementation( files("$rootDir/fineract-provider/build/classes/java/main/"), project(path: ':fineract-core', configuration: 'runtimeElements'), project(path: ':fineract-accounting', configuration: 'runtimeElements'), diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedLengthLoanProductIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedLengthLoanProductIntegrationTest.java index eaacc633ad7..82bcf8b8dd3 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedLengthLoanProductIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedLengthLoanProductIntegrationTest.java @@ -28,15 +28,12 @@ import org.apache.fineract.client.models.PutLoanProductsProductIdResponse; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -@ExtendWith(LoanTestLifecycleExtension.class) public class FixedLengthLoanProductIntegrationTest extends BaseLoanIntegrationTest { @Test diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstallmentLevelDelinquencyAPIIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstallmentLevelDelinquencyAPIIntegrationTests.java index ceb924bdcc9..12f456f3cac 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstallmentLevelDelinquencyAPIIntegrationTests.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstallmentLevelDelinquencyAPIIntegrationTests.java @@ -39,14 +39,11 @@ import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.SchedulerJobHelper; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; @Slf4j -@ExtendWith(LoanTestLifecycleExtension.class) public class InstallmentLevelDelinquencyAPIIntegrationTests extends BaseLoanIntegrationTest { private SchedulerJobHelper schedulerJobHelper = new SchedulerJobHelper(this.requestSpec); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java index cf2877dcae7..25e22e48fa1 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountChargeOffWithAdvancedPaymentAllocationTest.java @@ -64,7 +64,6 @@ import org.apache.fineract.integrationtests.common.funds.FundsResourceHandler; import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanProductHelper; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; import org.apache.fineract.integrationtests.common.system.CodeHelper; @@ -73,9 +72,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -@ExtendWith(LoanTestLifecycleExtension.class) public class LoanAccountChargeOffWithAdvancedPaymentAllocationTest extends BaseLoanIntegrationTest { private static final DateTimeFormatter DATE_FORMATTER = new DateTimeFormatterBuilder().appendPattern("dd MMMM yyyy").toFormatter(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountFraudTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountFraudTest.java index 1cca12cca1a..0fce637e6a6 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountFraudTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountFraudTest.java @@ -32,13 +32,10 @@ import org.apache.fineract.client.models.PutLoansLoanIdResponse; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.Utils; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; @Slf4j -@ExtendWith(LoanTestLifecycleExtension.class) public class LoanAccountFraudTest extends BaseLoanIntegrationTest { private static final double AMOUNT = 100.0; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java index 4a1c6d33ca8..696fafe63fb 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java @@ -35,17 +35,14 @@ import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.integrationtests.common.ClientHelper; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; @Slf4j -@ExtendWith(LoanTestLifecycleExtension.class) public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoanIntegrationTest { @Test diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java index 3378f7b5c95..63395b1b927 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java @@ -38,8 +38,10 @@ import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PostChargesResponse; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.integrationtests.common.BusinessStepHelper; import org.apache.fineract.integrationtests.common.ClientHelper; @@ -656,6 +658,128 @@ public void verifyEarlyLateRepaymentOnProgressiveLoanNextInstallmentAllocationRe }); } + @Test + public void verifyChargeCreationAfterMaturityDateOnInterestBearingProgressiveLoan() { + AtomicReference loanIdRef = new AtomicReference<>(); + runAt("1 January 2024", () -> { + PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(create4IProgressive() // + .recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) // + .currencyCode("USD")); + + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2024", 100.0, 7.0, 6, + null); + loanIdRef.set(loanId); + + disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + logLoanDetails(loanDetails); + + validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2024", 16.43, 0.0, 0.0, 0.58); + validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2024", 16.52, 0.0, 0.0, 0.49); + validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2024", 16.62, 0.0, 0.0, 0.39); + validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2024", 16.72, 0.0, 0.0, 0.29); + validateFullyUnpaidRepaymentPeriod(loanDetails, 5, "01 June 2024", 16.81, 0.0, 0.0, 0.20); + validateFullyUnpaidRepaymentPeriod(loanDetails, 6, "01 July 2024", 16.90, 0.0, 0.0, 0.10); + + }); + runAt("10 July 2024", () -> { + Long loanId = loanIdRef.get(); + + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + + // create charge + PostChargesResponse chargeResult = createCharge(10.0); + Assertions.assertNotNull(chargeResult); + Long chargeId = chargeResult.getResourceId(); + Assertions.assertNotNull(chargeId); + + // add charge after maturity + PostLoansLoanIdChargesResponse loanChargeResult = addLoanCharge(loanId, chargeId, "15 July 2024", 10.0); + Assertions.assertNotNull(loanChargeResult.getResourceId()); + + // verify N+1 installment in schedule + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + logLoanDetails(loanDetails); + + validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2024", 16.43, 0.0, 0.0, 0.58); + validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2024", 16.43, 0.0, 0.0, 0.58); + validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2024", 16.43, 0.0, 0.0, 0.58); + validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2024", 16.43, 0.0, 0.0, 0.58); + validateFullyUnpaidRepaymentPeriod(loanDetails, 5, "01 June 2024", 16.43, 0.0, 0.0, 0.58); + validateFullyUnpaidRepaymentPeriod(loanDetails, 6, "01 July 2024", 17.85, 0.0, 0.0, 0.58); + validateFullyUnpaidRepaymentPeriod(loanDetails, 7, "15 July 2024", 0.0, 10.0, 0.0, 0.0); + + Assertions.assertNotNull(loanDetails.getStatus()); + Assertions.assertEquals(300, loanDetails.getStatus().getId()); + }); + runAt("15 July 2024", () -> { + Long loanId = loanIdRef.get(); + + loanTransactionHelper.makeLoanRepayment("15 July 2024", 113.48F, loanId.intValue()); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + logLoanDetails(loanDetails); + + validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 3, "01 April 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 4, "01 May 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 5, "01 June 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 6, "01 July 2024", 17.85, 0.0, 0.0, 0.58, 18.43); + validateFullyPaidRepaymentPeriod(loanDetails, 7, "15 July 2024", 0.0, 10.0, 0.0, 0.0); + + Assertions.assertNotNull(loanDetails.getStatus()); + Assertions.assertEquals(600, loanDetails.getStatus().getId()); + }); + runAt("16 July 2024", () -> { + Long loanId = loanIdRef.get(); + + // create charge + PostChargesResponse chargeResult = createCharge(10.0); + Assertions.assertNotNull(chargeResult); + Long chargeId = chargeResult.getResourceId(); + Assertions.assertNotNull(chargeId); + + // add charge after maturity + PostLoansLoanIdChargesResponse loanChargeResult = addLoanCharge(loanId, chargeId, "20 July 2024", 15.0); + Assertions.assertNotNull(loanChargeResult.getResourceId()); + + // verify N+1 installment in schedule + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + logLoanDetails(loanDetails); + + validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 3, "01 April 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 4, "01 May 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 5, "01 June 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 6, "01 July 2024", 17.85, 0.0, 0.0, 0.58, 18.43); + validateRepaymentPeriod(loanDetails, 7, LocalDate.of(2024, 7, 20), 0.0, 0.0, 0.0, 25.0, 10.0, 15.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0); + + Assertions.assertNotNull(loanDetails.getStatus()); + Assertions.assertEquals(300, loanDetails.getStatus().getId()); + }); + runAt("20 July 2024", () -> { + Long loanId = loanIdRef.get(); + + loanTransactionHelper.makeLoanRepayment("20 July 2024", 15.0F, loanId.intValue()); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + logLoanDetails(loanDetails); + + validateFullyPaidRepaymentPeriod(loanDetails, 1, "01 February 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 2, "01 March 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 3, "01 April 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 4, "01 May 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 5, "01 June 2024", 16.43, 0.0, 0.0, 0.58, 17.01); + validateFullyPaidRepaymentPeriod(loanDetails, 6, "01 July 2024", 17.85, 0.0, 0.0, 0.58, 18.43); + validateFullyPaidRepaymentPeriod(loanDetails, 7, "20 July 2024", 0.0, 25.0, 0.0, 0.0); + + Assertions.assertNotNull(loanDetails.getStatus()); + Assertions.assertEquals(600, loanDetails.getStatus().getId()); + }); + } + @Test public void verifyEarlyLateRepaymentOnProgressiveLoanNextInstallmentAllocationRepayEmi() { AtomicReference loanIdRef = new AtomicReference<>(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanOriginationValidationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanOriginationValidationTest.java index 63ac43f35cb..d046c48f700 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanOriginationValidationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanOriginationValidationTest.java @@ -46,7 +46,6 @@ import org.apache.fineract.integrationtests.common.accounting.AccountHelper; import org.apache.fineract.integrationtests.common.charges.ChargesHelper; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; @@ -55,11 +54,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ExtendWith(LoanTestLifecycleExtension.class) public class LoanOriginationValidationTest extends BaseLoanIntegrationTest { private static final Logger LOG = LoggerFactory.getLogger(AdvancedPaymentAllocationLoanRepaymentScheduleTest.class); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPayOffTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPayOffTest.java index 164e4120a69..8f925721802 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPayOffTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPayOffTest.java @@ -24,13 +24,10 @@ import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.integrationtests.common.ClientHelper; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -@ExtendWith(LoanTestLifecycleExtension.class) public class LoanPayOffTest extends BaseLoanIntegrationTest { private Long clientId; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductChargeOffReasonMappingsTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductChargeOffReasonMappingsTest.java new file mode 100644 index 00000000000..95d8a735cbc --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductChargeOffReasonMappingsTest.java @@ -0,0 +1,272 @@ +/** + * 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.apache.fineract.integrationtests.common.funds.FundsResourceHandler.createFund; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.apache.fineract.client.models.AllowAttributeOverrides; +import org.apache.fineract.client.models.ChargeData; +import org.apache.fineract.client.models.ChargeToGLAccountMapper; +import org.apache.fineract.client.models.GetChargeOffReasonsToExpenseMappings; +import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings; +import org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PutLoanProductsProductIdRequest; +import org.apache.fineract.client.util.CallFailedRuntimeException; +import org.apache.fineract.integrationtests.common.BusinessStepHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; +import org.apache.fineract.integrationtests.common.system.CodeHelper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(LoanTestLifecycleExtension.class) +public class LoanProductChargeOffReasonMappingsTest extends BaseLoanIntegrationTest { + + private static final String CODE_VALUE_NAME = "ChargeOffReasons"; + + private static ResponseSpecification responseSpec; + private static RequestSpecification requestSpec; + private static LoanTransactionHelper loanTransactionHelper; + + @BeforeAll + public static void setup() { + Utils.initializeRESTAssured(); + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + requestSpec.header("Fineract-Platform-TenantId", "default"); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); + BusinessStepHelper businessStepHelper = new BusinessStepHelper(); + // setup COB Business Steps to prevent test failing due other integration test configurations + businessStepHelper.updateSteps("LOAN_CLOSE_OF_BUSINESS", "APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", + "CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES", + "EXTERNAL_ASSET_OWNER_TRANSFER", "ACCRUAL_ACTIVITY_POSTING"); + } + + @Test + public void testCreateLoanProductWithValidChargeOffReason() { + final String creationBusinessDay = "15 January 2023"; + runAt(creationBusinessDay, () -> { + Integer chargeOffReasons = createChargeOffReason(); + Long localLoanProductId = loanTransactionHelper.createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), 15L)) + .getResourceId(); + + Assertions.assertNotNull(localLoanProductId); + }); + } + + @Test + public void testUpdateLoanProductWithValidChargeOffReason() { + final String creationBusinessDay = "15 January 2023"; + runAt(creationBusinessDay, () -> { + Integer chargeOffReasons = createChargeOffReason(); + List chargeOffReasonsToExpenseMappings = new ArrayList<>(); + GetChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new GetChargeOffReasonsToExpenseMappings(); + getChargeOffReasonsToExpenseMappings.setChargeOffReasonCodeValueId(Long.valueOf(chargeOffReasons)); + getChargeOffReasonsToExpenseMappings.setExpenseGLAccountId(15L); + chargeOffReasonsToExpenseMappings.add(getChargeOffReasonsToExpenseMappings); + + Long localLoanProductId = loanTransactionHelper.updateLoanProduct(1L, + new PutLoanProductsProductIdRequest().locale("en").chargeOffReasonsToExpenseMappings(chargeOffReasonsToExpenseMappings)) + .getResourceId(); + + Assertions.assertNotNull(localLoanProductId); + }); + } + + @Test + public void testCreateLoanProductWithInvalidGLAccount() { + final String creationBusinessDay = "15 January 2023"; + runAt(creationBusinessDay, () -> { + try { + Integer chargeOffReasons = createChargeOffReason(); + loanTransactionHelper.createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), 9999L)); + } catch (CallFailedRuntimeException e) { + Assertions.assertTrue(e.getMessage().contains("validation.msg.glaccount.not.found")); + } + }); + } + + @Test + public void testCreateLoanProductWithInvalidChargeOffReason() { + final String creationBusinessDay = "15 January 2023"; + runAt(creationBusinessDay, () -> { + try { + loanTransactionHelper.createLoanProduct(loanProductsRequest(1L, 12L)); + } catch (CallFailedRuntimeException e) { + Assertions.assertTrue(e.getMessage().contains("validation.msg.chargeoffreason.invalid")); + } + }); + } + + private PostLoanProductsRequest loanProductsRequest(Long chargeOffReasonId, Long glAccountId) { + String name = Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6); + String shortName = Utils.uniqueRandomStringGenerator("", 4); + + List principalVariationsForBorrowerCycle = new ArrayList<>(); + List numberOfRepaymentVariationsForBorrowerCycle = new ArrayList<>(); + List interestRateVariationsForBorrowerCycle = new ArrayList<>(); + List charges = new ArrayList<>(); + List penaltyToIncomeAccountMappings = new ArrayList<>(); + List feeToIncomeAccountMappings = new ArrayList<>(); + + List chargeOffReasonsToExpenseMappings = new ArrayList<>(); + GetChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new GetChargeOffReasonsToExpenseMappings(); + getChargeOffReasonsToExpenseMappings.setChargeOffReasonCodeValueId(chargeOffReasonId); + getChargeOffReasonsToExpenseMappings.setExpenseGLAccountId(glAccountId); + chargeOffReasonsToExpenseMappings.add(getChargeOffReasonsToExpenseMappings); + + List paymentChannelToFundSourceMappings = new ArrayList<>(); + GetLoanPaymentChannelToFundSourceMappings loanPaymentChannelToFundSourceMappings = new GetLoanPaymentChannelToFundSourceMappings(); + loanPaymentChannelToFundSourceMappings.fundSourceAccountId(fundSource.getAccountID().longValue()); + loanPaymentChannelToFundSourceMappings.paymentTypeId(1L); + paymentChannelToFundSourceMappings.add(loanPaymentChannelToFundSourceMappings); + + final Integer fundId = createFund(requestSpec, responseSpec); + Assertions.assertNotNull(fundId); + + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + Assertions.assertNotNull(delinquencyBucketId); + + return new PostLoanProductsRequest()// + .name(name)// + .enableAccrualActivityPosting(true)// + .shortName(shortName)// + .description( + "LP1 with 12% DECLINING BALANCE interest, interest period: Daily, Interest recalculation-Daily, Compounding:none")// + .fundId(fundId.longValue())// + .startDate(null)// + .closeDate(null)// + .includeInBorrowerCycle(false)// + .currencyCode("EUR")// + .digitsAfterDecimal(2)// + .inMultiplesOf(1)// + .installmentAmountInMultiplesOf(1)// + .useBorrowerCycle(false)// + .minPrincipal(100.0)// + .principal(1000.0)// + .maxPrincipal(10000.0)// + .minNumberOfRepayments(1)// + .numberOfRepayments(1)// + .maxNumberOfRepayments(30)// + .isLinkedToFloatingInterestRates(false)// + .minInterestRatePerPeriod(0.0)// + .interestRatePerPeriod(12.0)// + .maxInterestRatePerPeriod(30.0)// + .interestRateFrequencyType(3)// + .repaymentEvery(30)// + .repaymentFrequencyType(0L)// + .principalVariationsForBorrowerCycle(principalVariationsForBorrowerCycle)// + .numberOfRepaymentVariationsForBorrowerCycle(numberOfRepaymentVariationsForBorrowerCycle)// + .interestRateVariationsForBorrowerCycle(interestRateVariationsForBorrowerCycle)// + .amortizationType(1)// + .interestType(0)// + .isEqualAmortization(false)// + .interestCalculationPeriodType(0)// + .transactionProcessingStrategyCode("mifos-standard-strategy")// + .daysInYearType(1)// + .daysInMonthType(1)// + .canDefineInstallmentAmount(true)// + .graceOnArrearsAgeing(3)// + .overdueDaysForNPA(179)// + .accountMovesOutOfNPAOnlyOnArrearsCompletion(false)// + .principalThresholdForLastInstallment(50)// + .allowVariableInstallments(false)// + .canUseForTopup(false)// + .holdGuaranteeFunds(false)// + .multiDisburseLoan(false)// + .allowAttributeOverrides(new AllowAttributeOverrides()// + .amortizationType(true)// + .interestType(true)// + .transactionProcessingStrategyCode(true)// + .interestCalculationPeriodType(true)// + .inArrearsTolerance(true)// + .repaymentEvery(true)// + .graceOnPrincipalAndInterestPayment(true)// + .graceOnArrearsAgeing(true)) + .outstandingLoanBalance(10000.0)// + .charges(charges)// + .accountingRule(3)// + + .fundSourceAccountId(suspenseAccount.getAccountID().longValue())// + .loanPortfolioAccountId(loansReceivableAccount.getAccountID().longValue())// + .transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())// + .interestOnLoanAccountId(interestIncomeAccount.getAccountID().longValue())// + .incomeFromFeeAccountId(feeIncomeAccount.getAccountID().longValue())// + .incomeFromPenaltyAccountId(penaltyIncomeAccount.getAccountID().longValue())// + .incomeFromRecoveryAccountId(recoveriesAccount.getAccountID().longValue())// + .writeOffAccountId(writtenOffAccount.getAccountID().longValue())// + .overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())// + .receivableInterestAccountId(interestReceivableAccount.getAccountID().longValue())// + .receivableFeeAccountId(feeReceivableAccount.getAccountID().longValue())// + .receivablePenaltyAccountId(penaltyReceivableAccount.getAccountID().longValue())// + .goodwillCreditAccountId(goodwillExpenseAccount.getAccountID().longValue())// + .incomeFromGoodwillCreditInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())// + .incomeFromGoodwillCreditFeesAccountId(feeChargeOffAccount.getAccountID().longValue())// + .incomeFromGoodwillCreditPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue())// + .incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())// + .incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())// + .chargeOffExpenseAccountId(chargeOffExpenseAccount.getAccountID().longValue())// + .chargeOffFraudExpenseAccountId(chargeOffFraudExpenseAccount.getAccountID().longValue())// + .incomeFromChargeOffPenaltyAccountId(penaltyChargeOffAccount.getAccountID().longValue())// + + .dateFormat("dd MMMM yyyy")// + .locale("en")// + .disallowExpectedDisbursements(false)// + .allowApprovedDisbursedAmountsOverApplied(false)// + .delinquencyBucketId(delinquencyBucketId.longValue())// + .paymentChannelToFundSourceMappings(paymentChannelToFundSourceMappings)// + .penaltyToIncomeAccountMappings(penaltyToIncomeAccountMappings)// + .chargeOffReasonsToExpenseMappings(chargeOffReasonsToExpenseMappings).feeToIncomeAccountMappings(feeToIncomeAccountMappings)// + .isInterestRecalculationEnabled(true)// + .preClosureInterestCalculationStrategy(1)// + .rescheduleStrategyMethod(3)// + .interestRecalculationCompoundingMethod(0)// + .recalculationRestFrequencyType(2)// + .recalculationRestFrequencyInterval(1)// + .allowPartialPeriodInterestCalcualtion(false);// + } + + private Integer createChargeOffReason() { + Integer chargeOffReasonId; + HashMap codes = CodeHelper.getCodeByName(requestSpec, responseSpec, CODE_VALUE_NAME); + if (codes.isEmpty()) { + CodeHelper.createCode(requestSpec, responseSpec, CODE_VALUE_NAME, ""); + } + codes = CodeHelper.getCodeByName(requestSpec, responseSpec, CODE_VALUE_NAME); + Integer codeId = (Integer) codes.get("id"); + HashMap codeValues = CodeHelper.getOrCreateCodeValueByCodeIdAndCodeName(requestSpec, responseSpec, codeId, + CODE_VALUE_NAME, 1); + chargeOffReasonId = (Integer) codeValues.get("id"); + return chargeOffReasonId; + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleTestWithDownpayment.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleTestWithDownpayment.java index abf0b2f88bf..b777b08440b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleTestWithDownpayment.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleTestWithDownpayment.java @@ -29,11 +29,8 @@ import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper; import org.apache.fineract.integrationtests.common.loans.LoanRescheduleRequestTestBuilder; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -@ExtendWith(LoanTestLifecycleExtension.class) public class LoanRescheduleTestWithDownpayment extends BaseLoanIntegrationTest { public static final BigDecimal DOWN_PAYMENT_PERCENTAGE_20 = new BigDecimal(20); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java index 90bb85c33b4..89ec953e51c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java @@ -56,7 +56,6 @@ import org.apache.fineract.integrationtests.common.SchedulerJobHelper; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.charges.ChargesHelper; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper; @@ -68,13 +67,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ExtendWith(LoanTestLifecycleExtension.class) public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrationTest { private static final Logger LOG = LoggerFactory.getLogger(LoanTransactionAccrualActivityPostingTest.class); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionInterestPaymentWaiverTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionInterestPaymentWaiverTest.java index b8bd25f0cb3..8c79e7f2c76 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionInterestPaymentWaiverTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionInterestPaymentWaiverTest.java @@ -72,7 +72,6 @@ import org.apache.fineract.integrationtests.common.accounting.AccountHelper; import org.apache.fineract.integrationtests.common.charges.ChargesHelper; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.system.CodeHelper; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; @@ -86,11 +85,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ExtendWith(LoanTestLifecycleExtension.class) public class LoanTransactionInterestPaymentWaiverTest extends BaseLoanIntegrationTest { private static final Logger LOG = LoggerFactory.getLogger(AdvancedPaymentAllocationLoanRepaymentScheduleTest.class); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionReverseReplayChargeOffTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionReverseReplayChargeOffTest.java index 08b4c3c9fea..41c1323575f 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionReverseReplayChargeOffTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionReverseReplayChargeOffTest.java @@ -34,12 +34,9 @@ import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.Account; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.system.CodeHelper; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -@ExtendWith(LoanTestLifecycleExtension.class) public class LoanTransactionReverseReplayChargeOffTest extends BaseLoanIntegrationTest { @Test diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoLoanDisbursalWithDownPaymentIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoLoanDisbursalWithDownPaymentIntegrationTest.java index 20f4837ac85..1aa2b5eac38 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoLoanDisbursalWithDownPaymentIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoLoanDisbursalWithDownPaymentIntegrationTest.java @@ -29,11 +29,8 @@ import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.integrationtests.common.ClientHelper; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -@ExtendWith(LoanTestLifecycleExtension.class) public class UndoLoanDisbursalWithDownPaymentIntegrationTest extends BaseLoanIntegrationTest { public static final BigDecimal DOWN_PAYMENT_PERCENTAGE = new BigDecimal(25); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index e9439c25922..75624a8c598 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -107,6 +107,7 @@ public class LoanProductTestBuilder { private List> feeToIncomeAccountMappings = null; private List> penaltyToIncomeAccountMappings = null; + private List> chargeOffReasonsToExpenseMappings = null; private Account feeAndPenaltyAssetAccount; private Boolean multiDisburseLoan = false; @@ -303,6 +304,10 @@ public HashMap build(final String chargeId, final Integer delinq map.put("penaltyToIncomeAccountMappings", this.penaltyToIncomeAccountMappings); } + if (this.chargeOffReasonsToExpenseMappings != null) { + map.put("chargeOffReasonsToExpenseMappings", this.chargeOffReasonsToExpenseMappings); + } + if (this.dueDaysForRepaymentEvent != null) { map.put("dueDaysForRepaymentEvent", this.dueDaysForRepaymentEvent); } @@ -796,6 +801,17 @@ public LoanProductTestBuilder withSupportedInterestRefundTypes(String... refundT return this; } + public LoanProductTestBuilder withChargeOffReasonsToExpenseMappings(final Long reasonId, final Long accountId) { + if (this.chargeOffReasonsToExpenseMappings == null) { + this.chargeOffReasonsToExpenseMappings = new ArrayList<>(); + } + Map newMap = new HashMap<>(); + newMap.put("chargeOffReasonCodeValueId", reasonId); + newMap.put("expenseGLAccountId", accountId); + this.chargeOffReasonsToExpenseMappings.add(newMap); + return this; + } + public String getTransactionProcessingStrategyCode() { return transactionProcessingStrategyCode; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java index 6eed20b579d..2f0cce3635c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java @@ -93,6 +93,35 @@ public static HashMap getCodeByName(final RequestSpecification r return code; } + public static HashMap getOrCreateCodeValueByCodeIdAndCodeName(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer codeId, final String codeName, final Integer position) { + + ArrayList> allCodeValues = CodeHelper.getAllCodeValuesByCodeId(requestSpec, responseSpec, codeId); + HashMap codesByName = filterCodesByName(allCodeValues, codeName); + + if (codesByName.isEmpty()) { + CodeHelper.createCodeValue(requestSpec, responseSpec, codeId, codeName, position); + allCodeValues = CodeHelper.getAllCodeValuesByCodeId(requestSpec, responseSpec, codeId); + } + + return filterCodesByName(allCodeValues, codeName); + } + + private static HashMap filterCodesByName(ArrayList> allCodeValues, String codeName) { + final HashMap codes = new HashMap<>(); + + for (HashMap map : allCodeValues) { + String name = (String) map.get("name"); + if (name.equals(codeName)) { + codes.put("id", map.get("id")); + codes.put("name", map.get("name")); + break; + } + } + + return codes; + } + public static HashMap retrieveOrCreateCodeValue(Integer codeId, final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { Integer codeValueId = null; @@ -119,6 +148,13 @@ public static ArrayList> getAllCodes(final RequestSpecif } + public static ArrayList> getAllCodeValuesByCodeId(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer codeId) { + + return Utils.performServerGet(requestSpec, responseSpec, + CODE_VALUE_URL.replace("[codeId]", codeId.toString()) + "?" + Utils.TENANT_IDENTIFIER, ""); + } + public static Object getSystemDefinedCodes(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { final String getResponse = given().spec(requestSpec).expect().spec(responseSpec).when() diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java index b8bdb42d620..de779040432 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/SearchExternalAssetOwnerTransferTest.java @@ -33,12 +33,9 @@ import org.apache.fineract.client.models.PostInitiateTransferResponse; import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; import org.apache.fineract.integrationtests.common.Utils; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; @Slf4j -@ExtendWith(LoanTestLifecycleExtension.class) public class SearchExternalAssetOwnerTransferTest extends ExternalAssetOwnerTransferTest { @Test diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java index c1f81ab1760..2df5de65690 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java @@ -30,13 +30,10 @@ import org.apache.fineract.integrationtests.BaseLoanIntegrationTest; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -@ExtendWith(LoanTestLifecycleExtension.class) public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest { public static final BigDecimal DOWN_PAYMENT_PERCENTAGE = new BigDecimal(25); diff --git a/oauth2-tests/dependencies.gradle b/oauth2-tests/dependencies.gradle index 4ae1b36dbae..022d9d526a8 100644 --- a/oauth2-tests/dependencies.gradle +++ b/oauth2-tests/dependencies.gradle @@ -20,7 +20,7 @@ dependencies { // testCompile dependencies are ONLY used in src/test, not src/main. // Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly! // - tomcat 'org.apache.tomcat:tomcat:10.1.31@zip' + tomcat 'org.apache.tomcat:tomcat:10.1.33@zip' testImplementation( files("$rootDir/fineract-provider/build/classes/java/main/"), project(path: ':fineract-provider', configuration: 'runtimeElements'), 'org.junit.jupiter:junit-jupiter-api', diff --git a/twofactor-tests/dependencies.gradle b/twofactor-tests/dependencies.gradle index 789860eb11d..3272600cce4 100644 --- a/twofactor-tests/dependencies.gradle +++ b/twofactor-tests/dependencies.gradle @@ -20,7 +20,7 @@ dependencies { // testCompile dependencies are ONLY used in src/test, not src/main. // Do NOT repeat dependencies which are ALREADY in implementation or runtimeOnly! // - tomcat 'org.apache.tomcat:tomcat:10.1.31@zip' + tomcat 'org.apache.tomcat:tomcat:10.1.33@zip' testImplementation( files("$rootDir/fineract-provider/build/classes/java/main/"), project(path: ':fineract-provider', configuration: 'runtimeElements'), 'org.junit.jupiter:junit-jupiter-api',