diff --git a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle index 99945682199..ff0baec78ae 100644 --- a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle +++ b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle @@ -37,6 +37,7 @@ dependencyManagement { mavenBom 'io.github.resilience4j:resilience4j-bom:2.4.0' mavenBom 'org.testcontainers:testcontainers-bom:1.21.4' mavenBom 'org.glassfish.jersey:jersey-bom:3.1.11' + mavenBom 'org.springframework.modulith:spring-modulith-bom:1.4.11' } dependencies { diff --git a/fineract-accounting/build.gradle b/fineract-accounting/build.gradle index 9f7ab3ab4e1..b9d1e72c74d 100644 --- a/fineract-accounting/build.gradle +++ b/fineract-accounting/build.gradle @@ -48,6 +48,11 @@ configurations { apply from: 'dependencies.gradle' +test { + maxHeapSize = '2g' + jvmArgs += ['-XX:MaxMetaspaceSize=1g'] +} + // Configuration for the modernizer plugin // https://github.com/andygoossens/gradle-modernizer-plugin modernizer { diff --git a/fineract-accounting/dependencies.gradle b/fineract-accounting/dependencies.gradle index 907e72eac20..7d62ab711b2 100644 --- a/fineract-accounting/dependencies.gradle +++ b/fineract-accounting/dependencies.gradle @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - dependencies { // Never use "compile" scope, but make all dependencies either 'implementation', 'runtimeOnly' or 'testCompile'. // Note that we never use 'api', because Fineract at least currently is a simple monolithic application ("WAR"), not a library. @@ -25,7 +24,8 @@ dependencies { // implementation dependencies are directly used (compiled against) in src/main (and src/test) // implementation(project(path: ':fineract-core')) - implementation(project(path: ':fineract-charge')) + testImplementation 'org.springframework.modulith:spring-modulith-core' + testImplementation("org.osgi:org.osgi.core:6.0.0") implementation('org.apache.avro:avro') implementation( project(path: ':fineract-avro-schemas') 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 34442ba2169..16294a83ebb 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 @@ -31,7 +31,6 @@ import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; -import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.paymenttype.domain.PaymentType; @Getter @@ -54,9 +53,8 @@ public class ProductToGLAccountMapping extends AbstractPersistableCustom { @JoinColumn(name = "payment_type", nullable = true) private PaymentType paymentType; - @ManyToOne - @JoinColumn(name = "charge_id", nullable = true) - private Charge charge; + @Column(name = "charge_id", nullable = true) + private Long chargeId; @Column(name = "product_type", nullable = true) private int productType; 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 885e5932141..65b9934f16e 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 @@ -30,12 +30,12 @@ public interface ProductToGLAccountMappingRepository ProductToGLAccountMapping findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId(Long productId, int productType, int financialAccountType, Long paymentType); - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId= :productId and mapping.productType= :productType and mapping.financialAccountType= :financialAccountType and mapping.charge.id= :chargeId") + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId= :productId and mapping.productType= :productType and mapping.financialAccountType= :financialAccountType and mapping.chargeId= :chargeId") ProductToGLAccountMapping findProductIdAndProductTypeAndFinancialAccountTypeAndChargeId(@Param("productId") Long productId, @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 and mapping.chargeOffReason is NULL and mapping.writeOffReason is NULL and mapping.capitalizedIncomeClassification is NULL and mapping.buydownFeeClassification 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.chargeId is NULL and mapping.chargeOffReason is NULL and mapping.writeOffReason is NULL and mapping.capitalizedIncomeClassification is NULL and mapping.buydownFeeClassification is NULL") ProductToGLAccountMapping findCoreProductToFinAccountMapping(@Param("productId") Long productId, @Param("productType") int productType, @Param("financialAccountType") int financialAccountType); @@ -49,14 +49,14 @@ List findAllPaymentTypeToFundSourceMappings(@Param("p /*** * The financial Account Type for income from interest will always be 4 ***/ - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=4 and mapping.charge is not NULL") + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=4 and mapping.chargeId is not NULL") List findAllFeeToIncomeAccountMappings(@Param("productId") Long productId, @Param("productType") int productType); /*** * The financial Account Type for income from interest will always be 5 ***/ - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=5 and mapping.charge is not NULL") + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=5 and mapping.chargeId is not NULL") List findAllPenaltyToIncomeAccountMappings(@Param("productId") Long productId, @Param("productType") int productType); @@ -77,19 +77,13 @@ List findAllWriteOffReasonsMappings(@Param("productId ProductToGLAccountMapping findChargeOffReasonMapping(@Param("productId") Long productId, @Param("productType") Integer productType, @Param("chargeOffReasonId") Long chargeOffReasonId); - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL AND mapping.writeOffReason IS NULL AND mapping.capitalizedIncomeClassification is NULL AND mapping.buydownFeeClassification is NULL") + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.chargeId IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL AND mapping.writeOffReason IS NULL AND mapping.capitalizedIncomeClassification is NULL AND mapping.buydownFeeClassification is NULL") List findAllRegularMappings(@Param("productId") Long productId, @Param("productType") Integer productType); @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.paymentType is not NULL") List findAllPaymentTypeMappings(@Param("productId") Long productId, @Param("productType") Integer productType); - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge.penalty = TRUE") - List findAllPenaltyMappings(@Param("productId") Long productId, @Param("productType") Integer productType); - - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge.penalty = FALSE") - List findAllFeeMappings(@Param("productId") Long productId, @Param("productType") Integer productType); - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.capitalizedIncomeClassification is not NULL") List findAllCapitalizedIncomeClassificationsMappings(@Param("productId") Long productId, @Param("productType") int productType); diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/AccountingChargeReadService.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/AccountingChargeReadService.java new file mode 100644 index 00000000000..0a2ef421afa --- /dev/null +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/AccountingChargeReadService.java @@ -0,0 +1,28 @@ +/** + * 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.accounting.producttoaccountmapping.service; + +import java.util.Collection; +import java.util.List; +import org.apache.fineract.portfolio.charge.data.ChargeData; + +public interface AccountingChargeReadService { + + List findChargesByIds(Collection chargeIds); +} 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 eb2a1998772..2aa9cdcee3b 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 @@ -48,8 +48,6 @@ 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; -import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.paymenttype.domain.PaymentType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; import org.apache.fineract.portfolio.paymenttype.exception.PaymentTypeNotFoundException; @@ -65,7 +63,6 @@ public class ProductToGLAccountMappingHelper { protected final GLAccountRepository accountRepository; protected final ProductToGLAccountMappingRepository accountMappingRepository; protected final FromJsonHelper fromApiJsonHelper; - private final ChargeRepositoryWrapper chargeRepositoryWrapper; protected final GLAccountRepositoryWrapper accountRepositoryWrapper; private final PaymentTypeRepository paymentTypeRepository; private final CodeValueRepository codeValueRepository; @@ -344,7 +341,7 @@ public void updateChargeToIncomeAccountMappings(final JsonCommand command, final **/ else { for (final ProductToGLAccountMapping chargeToIncomeAccountMapping : existingChargeToIncomeAccountMappings) { - final Long currentCharge = chargeToIncomeAccountMapping.getCharge().getId(); + final Long currentCharge = chargeToIncomeAccountMapping.getChargeId(); existingCharges.add(currentCharge); // update existing mappings (if required) if (inputChargeToIncomeAccountMap.containsKey(currentCharge)) { @@ -613,10 +610,6 @@ private void savePaymentChannelToFundSourceMapping(final Long productId, final L */ private void saveChargeToFundSourceMapping(final Long productId, final Long chargeId, final Long incomeAccountId, final PortfolioProductType portfolioProductType, final boolean isPenalty) { - final Charge charge = this.chargeRepositoryWrapper.findOneWithNotFoundDetection(chargeId); - - // TODO Vishwas: Need to validate if given charge is fee or Penalty - // based on input condition GLAccount glAccount; /** @@ -635,7 +628,7 @@ private void saveChargeToFundSourceMapping(final Long productId, final Long char } final ProductToGLAccountMapping accountMapping = new ProductToGLAccountMapping().setGlAccount(glAccount).setProductId(productId) .setProductType(portfolioProductType.getValue()).setFinancialAccountType(placeHolderAccountType.getValue()) - .setCharge(charge); + .setChargeId(chargeId); this.accountMappingRepository.saveAndFlush(accountMapping); } diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java index 5cbd6dfffe8..c19c00ac359 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java @@ -22,6 +22,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan; @@ -36,6 +38,7 @@ import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.accounting.common.AccountingValidations; import org.apache.fineract.accounting.glaccount.data.GLAccountData; +import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.accounting.producttoaccountmapping.data.AdvancedMappingToExpenseAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData; @@ -56,6 +59,7 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; private final CodeValueMapper codeValueMapper; + private final AccountingChargeReadService accountingChargeReadService; @Override public Map fetchAccountMappingDetailsForLoanProduct(final Long loanProductId, final Integer accountingType) { @@ -258,18 +262,23 @@ public List fetchPenaltyToIncomeAccountMappingsForSavin private List fetchChargeToIncomeAccountMappings(final PortfolioProductType portfolioProductType, final Long loanProductId, final boolean penalty) { final List mappings = penalty - ? productToGLAccountMappingRepository.findAllPenaltyMappings(loanProductId, portfolioProductType.getValue()) - : productToGLAccountMappingRepository.findAllFeeMappings(loanProductId, portfolioProductType.getValue()); + ? productToGLAccountMappingRepository.findAllPenaltyToIncomeAccountMappings(loanProductId, portfolioProductType.getValue()) + : productToGLAccountMappingRepository.findAllFeeToIncomeAccountMappings(loanProductId, portfolioProductType.getValue()); + if (mappings.isEmpty()) { + return null; + } + + final List chargeIds = mappings.stream().map(ProductToGLAccountMapping::getChargeId).toList(); + final Map chargesById = accountingChargeReadService.findChargesByIds(chargeIds).stream() + .collect(Collectors.toMap(ChargeData::getId, Function.identity())); - List chargeToGLAccountMappers = mappings.isEmpty() ? null : new ArrayList<>(); + final List chargeToGLAccountMappers = new ArrayList<>(); for (final ProductToGLAccountMapping mapping : mappings) { - final GLAccountData gLAccountData = new GLAccountData().setId(mapping.getGlAccount().getId()) - .setName(mapping.getGlAccount().getName()).setGlCode(mapping.getGlAccount().getGlCode()); - final ChargeData chargeData = ChargeData.builder().id(mapping.getCharge().getId()).name(mapping.getCharge().getName()) - .penalty(mapping.getCharge().isPenalty()).build(); - final ChargeToGLAccountMapper chargeToGLAccountMapper = new ChargeToGLAccountMapper().setCharge(chargeData) - .setIncomeAccount(gLAccountData); - chargeToGLAccountMappers.add(chargeToGLAccountMapper); + final GLAccount glAccount = mapping.getGlAccount(); + final GLAccountData gLAccountData = new GLAccountData().setId(glAccount.getId()).setName(glAccount.getName()) + .setGlCode(glAccount.getGlCode()); + chargeToGLAccountMappers + .add(new ChargeToGLAccountMapper().setCharge(chargesById.get(mapping.getChargeId())).setIncomeAccount(gLAccountData)); } return chargeToGLAccountMappers; } 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 d586552be6c..3826e847c01 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 @@ -33,7 +33,6 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; -import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; import org.springframework.stereotype.Component; @@ -42,16 +41,12 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa public SavingsProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, - final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper, - final PaymentTypeRepository paymentTypeRepository, final CodeValueRepository codeValueRepository) { - super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper, - paymentTypeRepository, codeValueRepository); + final GLAccountRepositoryWrapper accountRepositoryWrapper, final PaymentTypeRepository paymentTypeRepository, + final CodeValueRepository codeValueRepository) { + super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, accountRepositoryWrapper, paymentTypeRepository, + codeValueRepository); } - /*** - * Set of abstractions for saving Saving Products to GL Account Mappings - ***/ - public void saveSavingsToAssetAccountMapping(final JsonElement element, final String paramName, final Long productId, final int placeHolderTypeId) { saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.ASSET, PortfolioProductType.SAVING); 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 0c8b81f6be2..7fb3e5f58cc 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 @@ -32,7 +32,6 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; -import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; import org.springframework.stereotype.Component; @@ -41,10 +40,10 @@ public class ShareProductToGLAccountMappingHelper extends ProductToGLAccountMapp public ShareProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, - final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper, - final PaymentTypeRepository paymentTypeRepository, final CodeValueRepository codeValueRepository) { - super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper, - paymentTypeRepository, codeValueRepository); + final GLAccountRepositoryWrapper accountRepositoryWrapper, final PaymentTypeRepository paymentTypeRepository, + final CodeValueRepository codeValueRepository) { + super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, accountRepositoryWrapper, paymentTypeRepository, + codeValueRepository); } /*** diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/WorkingCapitalLoanProductAdvancedAccountingReadHelper.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/WorkingCapitalLoanProductAdvancedAccountingReadHelper.java index de9f891e197..16300f0a656 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/WorkingCapitalLoanProductAdvancedAccountingReadHelper.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/WorkingCapitalLoanProductAdvancedAccountingReadHelper.java @@ -20,8 +20,12 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.glaccount.data.GLAccountData; +import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.accounting.producttoaccountmapping.data.AdvancedMappingToExpenseAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; @@ -40,6 +44,7 @@ public class WorkingCapitalLoanProductAdvancedAccountingReadHelper { private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; private final CodeValueMapper codeValueMapper; + private final AccountingChargeReadService accountingChargeReadService; public List fetchPaymentTypeToFundSourceMappings(final Long wcLoanProductId) { final List mappings = productToGLAccountMappingRepository.findAllPaymentTypeMappings(wcLoanProductId, @@ -74,20 +79,26 @@ public List fetchWriteOffReasonMappings(fin } private List fetchChargeToIncomeMappings(final Long wcLoanProductId, final boolean penalty) { + final int productType = PortfolioProductType.WORKING_CAPITAL_LOAN.getValue(); final List mappings = penalty - ? productToGLAccountMappingRepository.findAllPenaltyMappings(wcLoanProductId, - PortfolioProductType.WORKING_CAPITAL_LOAN.getValue()) - : productToGLAccountMappingRepository.findAllFeeMappings(wcLoanProductId, - PortfolioProductType.WORKING_CAPITAL_LOAN.getValue()); + ? productToGLAccountMappingRepository.findAllPenaltyToIncomeAccountMappings(wcLoanProductId, productType) + : productToGLAccountMappingRepository.findAllFeeToIncomeAccountMappings(wcLoanProductId, productType); + if (mappings.isEmpty()) { + return null; + } + + final List chargeIds = mappings.stream().map(ProductToGLAccountMapping::getChargeId).toList(); + final Map chargesById = accountingChargeReadService.findChargesByIds(chargeIds).stream() + .collect(Collectors.toMap(ChargeData::getId, Function.identity())); + final List result = new ArrayList<>(); for (final ProductToGLAccountMapping mapping : mappings) { - final GLAccountData gLAccountData = new GLAccountData().setId(mapping.getGlAccount().getId()) - .setName(mapping.getGlAccount().getName()).setGlCode(mapping.getGlAccount().getGlCode()); - final ChargeData chargeData = ChargeData.builder().id(mapping.getCharge().getId()).name(mapping.getCharge().getName()) - .penalty(mapping.getCharge().isPenalty()).build(); - result.add(new ChargeToGLAccountMapper().setCharge(chargeData).setIncomeAccount(gLAccountData)); + final GLAccount glAccount = mapping.getGlAccount(); + final GLAccountData gLAccountData = new GLAccountData().setId(glAccount.getId()).setName(glAccount.getName()) + .setGlCode(glAccount.getGlCode()); + result.add(new ChargeToGLAccountMapper().setCharge(chargesById.get(mapping.getChargeId())).setIncomeAccount(gLAccountData)); } - return result.isEmpty() ? null : result; + return result; } private List fetchReasonMappings(final List mappings) { diff --git a/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java b/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java new file mode 100644 index 00000000000..57af11f494c --- /dev/null +++ b/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java @@ -0,0 +1,148 @@ +/** + * 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.accounting; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.base.Splitter; +import com.tngtech.archunit.core.domain.JavaClass; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.modulith.core.ApplicationModule; +import org.springframework.modulith.core.ApplicationModuleDependency; +import org.springframework.modulith.core.ApplicationModules; + +class AccountingCrossFeatureBoundaryTest { + + private static final Logger LOG = LoggerFactory.getLogger(AccountingCrossFeatureBoundaryTest.class); + + private static final String BASE = "org.apache.fineract"; + private static final String ACCOUNTING_PACKAGE = "org.apache.fineract.accounting"; + + private static final Set FOUNDATION_ARTIFACTS = Set.of("fineract-core", "fineract-command"); + + private static final Pattern FINERACT_ARTIFACT = Pattern.compile("(?<=/)fineract-[a-z0-9-]+"); + + private static ApplicationModules modules; + + private static ApplicationModules modules() { + if (modules == null) { + modules = ApplicationModules.of(BASE); + } + return modules; + } + + private static ApplicationModule accountingModule() { + return modules().stream() // + .filter(module -> module.getBasePackage().getName().equals(ACCOUNTING_PACKAGE)) // + .findFirst() // + .orElseThrow(() -> new IllegalStateException("Accounting module not found in the model")); + } + + private static String featureKey(String typeName) { + String prefix = BASE + "."; + if (!typeName.startsWith(prefix)) { + return typeName; + } + List parts = Splitter.on('.').splitToList(typeName.substring(prefix.length())); + return parts.size() >= 2 ? parts.get(0) + "." + parts.get(1) : parts.get(0); + } + + private static String owningArtifact(JavaClass type) { + return type.getSource().map(source -> source.getUri().toString()).map(uri -> { + Matcher matcher = FINERACT_ARTIFACT.matcher(uri); + String artifact = null; + while (matcher.find()) { + artifact = matcher.group(); + } + return artifact == null ? uri : artifact.replaceAll("-\\d.*$", ""); + }).orElse("(unknown-source)"); + } + + private static boolean isFoundation(JavaClass type) { + return FOUNDATION_ARTIFACTS.contains(owningArtifact(type)); + } + + @Test + @EnabledIfSystemProperty(named = "accounting.boundary.report", matches = "true") + void printAccountingCrossFeatureDependencyReport() { + Map> sourceTypeToTargets = new TreeMap<>(); + Set violationFeatures = new TreeSet<>(); + Set allowedFromCore = new TreeSet<>(); + + accountingModule().getDirectDependencies(modules()).stream().forEach((ApplicationModuleDependency dependency) -> { + JavaClass targetType = dependency.getTargetType(); + String sourceType = dependency.getSourceType().getName(); + String featureKey = featureKey(targetType.getName()); + String artifact = owningArtifact(targetType); + boolean foundation = isFoundation(targetType); + + String label = featureKey + " [" + artifact + (foundation ? " : foundation]" : " : VIOLATION]"); + sourceTypeToTargets.computeIfAbsent(sourceType, key -> new TreeSet<>()).add(label); + + if (foundation) { + allowedFromCore.add(featureKey + " (" + artifact + ")"); + } else { + violationFeatures.add(featureKey + " (" + artifact + ")"); + } + }); + + LOG.info("==== Accounting cross-feature dependency report ===="); + LOG.info("base package : {}", BASE); + LOG.info("-- source type -> referenced feature packages [owning artifact : status] --"); + + sourceTypeToTargets.forEach((source, targets) -> { + LOG.info("source type : {}", source); + LOG.info("referenced targets : {}", targets); + }); + + LOG.info("-- allowed dependencies --"); + LOG.info("allowed from core : {}", allowedFromCore); + LOG.info("-- dependency violations --"); + LOG.info("violation features : {}", violationFeatures); + } + + @Test + void accountingMustNotImportOtherFeatureModules() { + Set featureDependencies = new TreeSet<>(); + + accountingModule().getDirectDependencies(modules()).stream().forEach((ApplicationModuleDependency dependency) -> { + JavaClass targetType = dependency.getTargetType(); + if (!isFoundation(targetType)) { + featureDependencies.add(featureKey(targetType.getName()) + " (" + owningArtifact(targetType) + ")"); + } + }); + + assertThat(featureDependencies) // + .as("Accounting may depend only on fineract-core and fineract-command. Types referenced from " + + "other feature modules must be reached via core read-contracts / DTOs, command/event " + + "boundaries, or by-id references (or the shared type moved into core). Offending " + + "feature packages (with owning artifact): %s", featureDependencies) // + .isEmpty(); + } +} diff --git a/fineract-charge/dependencies.gradle b/fineract-charge/dependencies.gradle index 13291535f11..18af600992a 100644 --- a/fineract-charge/dependencies.gradle +++ b/fineract-charge/dependencies.gradle @@ -25,6 +25,7 @@ dependencies { // implementation dependencies are directly used (compiled against) in src/main (and src/test) // implementation(project(path: ':fineract-core')) + implementation(project(path: ':fineract-accounting')) implementation(project(path: ':fineract-tax')) implementation( diff --git a/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/AccountingChargeReadServiceImpl.java b/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/AccountingChargeReadServiceImpl.java new file mode 100644 index 00000000000..5b78c15cd6c --- /dev/null +++ b/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/AccountingChargeReadServiceImpl.java @@ -0,0 +1,47 @@ +/** + * 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.portfolio.charge.service; + +import java.util.Collection; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.accounting.producttoaccountmapping.service.AccountingChargeReadService; +import org.apache.fineract.portfolio.charge.data.ChargeData; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeRepository; +import org.springframework.stereotype.Service; + +/** + * Charge module implementation of the contract declared by the accounting module. Keeping the implementation here means + * the accounting module depends only on the interface it owns, and the charge module supplies the details at runtime. + */ +@Service +@RequiredArgsConstructor +public class AccountingChargeReadServiceImpl implements AccountingChargeReadService { + + private final ChargeRepository chargeRepository; + + @Override + public List findChargesByIds(final Collection chargeIds) { + if (chargeIds == null || chargeIds.isEmpty()) { + return List.of(); + } + return chargeRepository.findAllById(chargeIds).stream().map(Charge::toData).toList(); + } +} 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 78e5b2ce6f8..1db5f3e7ede 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 @@ -38,7 +38,6 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; -import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; import org.springframework.stereotype.Component; @@ -47,10 +46,10 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi public LoanProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, - final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper, - final PaymentTypeRepository paymentTypeRepository, final CodeValueRepository codeValueRepository) { - super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper, - paymentTypeRepository, codeValueRepository); + final GLAccountRepositoryWrapper accountRepositoryWrapper, final PaymentTypeRepository paymentTypeRepository, + final CodeValueRepository codeValueRepository) { + super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, accountRepositoryWrapper, paymentTypeRepository, + codeValueRepository); } /*** 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 5e29e5ca165..966056e39e4 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 @@ -21,6 +21,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountingRuleParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.isDormancyTrackingActiveParamName; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import java.util.HashMap; import java.util.Locale; @@ -41,6 +42,7 @@ import org.apache.fineract.accounting.producttoaccountmapping.service.ShareProductToGLAccountMappingHelper; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.springframework.stereotype.Service; @@ -55,11 +57,13 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc private final LoanProductToGLAccountMappingHelper loanProductToGLAccountMappingHelper; private final SavingsProductToGLAccountMappingHelper savingsProductToGLAccountMappingHelper; private final ShareProductToGLAccountMappingHelper shareProductToGLAccountMappingHelper; + private final ChargeRepositoryWrapper chargeRepositoryWrapper; @Override @Transactional public void createLoanProductToGLAccountMapping(final Long loanProductId, final JsonCommand command) { final JsonElement element = this.fromApiJsonHelper.parse(command.json()); + validateChargesExist(element); final Integer accountingRuleTypeId = this.fromApiJsonHelper.extractIntegerNamed("accountingRule", element, Locale.getDefault()); final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(accountingRuleTypeId); boolean merchantBuyDownFee = true; @@ -311,6 +315,7 @@ private void saveSavingsBaseAccountMapping(final Long savingProductId, final Dep public void createSavingProductToGLAccountMapping(final Long savingProductId, final JsonCommand command, DepositAccountType accountType) { final JsonElement element = this.fromApiJsonHelper.parse(command.json()); + validateChargesExist(element); final Integer accountingRuleTypeId = this.fromApiJsonHelper.extractIntegerNamed(accountingRuleParamName, element, Locale.getDefault()); @@ -354,6 +359,7 @@ public void createShareProductToGLAccountMapping(final Long shareProductId, fina this.deserializer.validateForShareProductCreate(command.json()); final JsonElement element = this.fromApiJsonHelper.parse(command.json()); + validateChargesExist(element); final Integer accountingRuleTypeId = this.fromApiJsonHelper.extractIntegerNamed(accountingRuleParamName, element, Locale.getDefault()); final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(accountingRuleTypeId); @@ -415,6 +421,7 @@ public Map updateLoanProductToGLAccountMapping(final Long loanPr accountingRuleType); } /*** else examine and update individual changes ***/ else { + validateChargesExist(element); this.loanProductToGLAccountMappingHelper.handleChangesToLoanProductToGLAccountMappings(loanProductId, changes, element, accountingRuleType, enableIncomeCapitalization, enableBuyDownFee, merchantBuyDownFee); this.loanProductToGLAccountMappingHelper.updatePaymentChannelToFundSourceMappings(command, element, loanProductId, changes); @@ -454,6 +461,7 @@ public Map updateSavingsProductToGLAccountMapping(final Long sav accountingRuleType); } /*** else examine and update individual changes ***/ else { + validateChargesExist(element); this.savingsProductToGLAccountMappingHelper.handleChangesToSavingsProductToGLAccountMappings(savingsProductId, changes, element, accountingRuleType); this.savingsProductToGLAccountMappingHelper.updatePaymentChannelToFundSourceMappings(command, element, savingsProductId, @@ -485,6 +493,7 @@ public Map updateShareProductToGLAccountMapping(final Long share accountingRuleType); } /*** else examine and update individual changes ***/ else { + validateChargesExist(element); this.shareProductToGLAccountMappingHelper.handleChangesToSharesProductToGLAccountMappings(shareProductId, changes, element, accountingRuleType); this.shareProductToGLAccountMappingHelper.updatePaymentChannelToFundSourceMappings(command, element, shareProductId, changes); @@ -492,4 +501,24 @@ public Map updateShareProductToGLAccountMapping(final Long share } return changes; } + + private void validateChargesExist(final JsonElement element) { + validateChargesExist(element, LoanProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue()); + validateChargesExist(element, LoanProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue()); + } + + private void validateChargesExist(final JsonElement element, final String arrayName) { + final JsonArray chargeToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayName, element); + if (chargeToIncomeAccountMappingArray == null) { + return; + } + for (int i = 0; i < chargeToIncomeAccountMappingArray.size(); i++) { + final JsonElement chargeIdElement = chargeToIncomeAccountMappingArray.get(i).getAsJsonObject() + .get(LoanProductAccountingParams.CHARGE_ID.getValue()); + // A missing chargeId is reported by the mapping helper as a mandatory field error, so it is skipped here. + if (chargeIdElement != null && !chargeIdElement.isJsonNull()) { + this.chargeRepositoryWrapper.findOneWithNotFoundDetection(chargeIdElement.getAsLong()); + } + } + } }