From 62c71a76932ba435128fec040fc578d4ccc8ea52 Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Thu, 9 Jul 2026 00:33:59 +0530 Subject: [PATCH 1/7] FINERACT-2646: Decouple accounting from charge via chargeId reference --- fineract-accounting/build.gradle | 5 + fineract-accounting/dependencies.gradle | 3 +- .../domain/ProductToGLAccountMapping.java | 6 +- .../ProductToGLAccountMappingRepository.java | 16 +- .../ProductToGLAccountMappingHelper.java | 8 +- ...AccountMappingReadPlatformServiceImpl.java | 37 +++- ...avingsProductToGLAccountMappingHelper.java | 13 +- .../ShareProductToGLAccountMappingHelper.java | 9 +- ...anProductAdvancedAccountingReadHelper.java | 34 ++- .../AccountingCrossFeatureBoundaryTest.java | 200 ++++++++++++++++++ .../LoanProductToGLAccountMappingHelper.java | 9 +- 11 files changed, 279 insertions(+), 61 deletions(-) create mode 100644 fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java 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..310cae68907 100644 --- a/fineract-accounting/dependencies.gradle +++ b/fineract-accounting/dependencies.gradle @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - +ext['spring-modulith.version'] = '1.4.11' 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,6 +25,7 @@ dependencies { // implementation dependencies are directly used (compiled against) in src/main (and src/test) // implementation(project(path: ':fineract-core')) + testImplementation 'org.springframework.modulith:spring-modulith-core' implementation(project(path: ':fineract-charge')) implementation('org.apache.avro:avro') implementation( 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/ProductToGLAccountMappingHelper.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java index eb2a1998772..89f8880fe0a 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,7 +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 @@ -635,7 +631,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..a0c749a08ea 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 @@ -36,6 +36,8 @@ 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.glaccount.domain.GLAccountRepository; import org.apache.fineract.accounting.producttoaccountmapping.data.AdvancedMappingToExpenseAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData; @@ -47,6 +49,7 @@ import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @Slf4j @@ -56,6 +59,8 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; private final CodeValueMapper codeValueMapper; + private final GLAccountRepository glAccountRepository; + private final JdbcTemplate jdbcTemplate; @Override public Map fetchAccountMappingDetailsForLoanProduct(final Long loanProductId, final Integer accountingType) { @@ -257,16 +262,27 @@ 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()); - - List chargeToGLAccountMappers = mappings.isEmpty() ? null : 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(); + // The accounting module is decoupled from the charge domain, so we cannot navigate a Charge association in + // JPQL. We read the charge -> income-account rows via JdbcTemplate instead of a JPA native query: EclipseLink + // does not bind Spring Data ":name" parameters in native queries (it expects "#name"), which caused the raw + // markers to reach the database. JdbcTemplate uses plain JDBC positional binding and works on all supported + // databases. Column order: [0] gl_account_id, [1] charge id, [2] charge name. The penalty flag is known from + // the method argument, so it is inlined as a boolean literal (never user input) rather than bound. + final String sql = "select apm.gl_account_id, c.id, c.name " + + "from acc_product_mapping apm inner join m_charge c on c.id = apm.charge_id " + + "where apm.product_id = ? and apm.product_type = ? and c.is_penalty = " + penalty; + final List rows = jdbcTemplate.query(sql, (rs, rowNum) -> new Object[] { rs.getLong(1), rs.getLong(2), rs.getString(3) }, + loanProductId, portfolioProductType.getValue()); + + List chargeToGLAccountMappers = rows.isEmpty() ? null : new ArrayList<>(); + for (final Object[] row : rows) { + final Long glAccountId = (Long) row[0]; + final Long chargeId = (Long) row[1]; + final String chargeName = (String) row[2]; + final GLAccount glAccount = glAccountRepository.findById(glAccountId).orElseThrow(); + final GLAccountData gLAccountData = new GLAccountData().setId(glAccount.getId()).setName(glAccount.getName()) + .setGlCode(glAccount.getGlCode()); + final ChargeData chargeData = ChargeData.builder().id(chargeId).name(chargeName).penalty(penalty).build(); final ChargeToGLAccountMapper chargeToGLAccountMapper = new ChargeToGLAccountMapper().setCharge(chargeData) .setIncomeAccount(gLAccountData); chargeToGLAccountMappers.add(chargeToGLAccountMapper); @@ -312,6 +328,7 @@ private List fetchClassificationMappings(final Po : productToGLAccountMappingRepository.findAllBuyDownFeeClassificationsMappings(loanProductId, portfolioProductType.getValue()); + productToGLAccountMappingRepository.findAllChargeOffReasonsMappings(loanProductId, portfolioProductType.getValue()); List classificationToGLAccountMappers = mappings.isEmpty() ? null : new ArrayList<>(); for (final ProductToGLAccountMapping mapping : mappings) { final Long glAccountId = mapping.getGlAccount().getId(); 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..70748520699 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 @@ -22,6 +22,8 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.glaccount.data.GLAccountData; +import org.apache.fineract.accounting.glaccount.domain.GLAccount; +import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository; import org.apache.fineract.accounting.producttoaccountmapping.data.AdvancedMappingToExpenseAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; @@ -32,6 +34,7 @@ import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component @@ -40,6 +43,8 @@ public class WorkingCapitalLoanProductAdvancedAccountingReadHelper { private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; private final CodeValueMapper codeValueMapper; + private final GLAccountRepository glAccountRepository; + private final JdbcTemplate jdbcTemplate; public List fetchPaymentTypeToFundSourceMappings(final Long wcLoanProductId) { final List mappings = productToGLAccountMappingRepository.findAllPaymentTypeMappings(wcLoanProductId, @@ -74,17 +79,26 @@ public List fetchWriteOffReasonMappings(fin } private List fetchChargeToIncomeMappings(final Long wcLoanProductId, final boolean penalty) { - final List mappings = penalty - ? productToGLAccountMappingRepository.findAllPenaltyMappings(wcLoanProductId, - PortfolioProductType.WORKING_CAPITAL_LOAN.getValue()) - : productToGLAccountMappingRepository.findAllFeeMappings(wcLoanProductId, - PortfolioProductType.WORKING_CAPITAL_LOAN.getValue()); + // The charge name lives on m_charge, not on ProductToGLAccountMapping (which only stores charge_id), and the + // accounting module is decoupled from the charge domain, so we read the charge -> income-account rows via + // JdbcTemplate rather than a JPA native query (EclipseLink does not bind Spring Data ":name" markers in native + // queries). Column order: [0] gl_account_id, [1] charge id, [2] charge name. The penalty flag is known from the + // method argument, so it is inlined as a boolean literal (never user input) rather than bound. + final String sql = "select apm.gl_account_id, c.id, c.name " + + "from acc_product_mapping apm inner join m_charge c on c.id = apm.charge_id " + + "where apm.product_id = ? and apm.product_type = ? and c.is_penalty = " + penalty; + final List rows = jdbcTemplate.query(sql, (rs, rowNum) -> new Object[] { rs.getLong(1), rs.getLong(2), rs.getString(3) }, + wcLoanProductId, PortfolioProductType.WORKING_CAPITAL_LOAN.getValue()); + 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(); + for (final Object[] row : rows) { + final Long glAccountId = (Long) row[0]; + final Long chargeId = (Long) row[1]; + final String chargeName = (String) row[2]; + final GLAccount glAccount = glAccountRepository.findById(glAccountId).orElseThrow(); + final GLAccountData gLAccountData = new GLAccountData().setId(glAccount.getId()).setName(glAccount.getName()) + .setGlCode(glAccount.getGlCode()); + final ChargeData chargeData = ChargeData.builder().id(chargeId).name(chargeName).penalty(penalty).build(); result.add(new ChargeToGLAccountMapper().setCharge(chargeData).setIncomeAccount(gLAccountData)); } return result.isEmpty() ? null : result; 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..0c8899b6c77 --- /dev/null +++ b/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java @@ -0,0 +1,200 @@ +/** + * 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.tngtech.archunit.core.domain.JavaClass; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.modulith.core.ApplicationModule; +import org.springframework.modulith.core.ApplicationModuleDependency; +import org.springframework.modulith.core.ApplicationModules; + +/** + * Cross-feature boundary check for the accounting feature, using Spring Modulith. + *

+ * The model is built with base {@code org.apache.fineract} so every other top-level Fineract area on the classpath is + * visible. A referenced type is "allowed foundation" when it physically lives in {@code fineract-core} or + * {@code fineract-command} — the two modules the outline lets every feature depend on. Crucially, this is decided by + * the artifact the referenced type is loaded from, not by its package name: {@code fineract-core} contains + * shared types under {@code infrastructure.*} but also under {@code organisation.*}, {@code portfolio.*}, etc. + * (DTOs/enums that were relocated to core). A package-name check would wrongly flag those; an artifact check does not. + *

+ * The owning artifact is read from each type's source location (ArchUnit records where every class was imported from). + * When accounting is built, {@code fineract-core} and {@code fineract-command} are on the classpath as jars, so a core + * type resolves to a URI containing {@code fineract-core} and a command type to one containing + * {@code fineract-command}. Anything loaded from any other {@code fineract-*} artifact is a genuine cross-feature + * dependency and is reported as a violation, to be fixed at the source (move the shared type into core, or reach the + * feature via a core read-contract / command / event / by-id reference) rather than by widening an allow-list. + *

+ * Because this test lives in the {@code fineract-accounting} module, it runs with only accounting and its own + * dependencies on the classpath, so it reports exactly the cross-feature edges that originate in the + * {@code fineract-accounting} jar (the work for FINERACT-2646). + */ +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"; + + /** + * The only artifacts every feature may depend on (outline: fineract-core + fineract-command). Membership is tested + * against the referenced type's NORMALISED owning artifact (exact set match), so a type counts as foundation purely + * by living in one of these jars/modules, whatever its package happens to be. + */ + private static final Set FOUNDATION_ARTIFACTS = Set.of("fineract-core", "fineract-command"); + + // Match a real module path segment: "fineract-..." that begins right after a "/". This deliberately does + // NOT match the parent checkout folder (e.g. "apache-fineract-modulith"), where "fineract-modulith" is + // preceded by "apache-", not "/", and would otherwise become the first match for every type. + private static final Pattern FINERACT_ARTIFACT = Pattern.compile("(?<=/)fineract-[a-z0-9-]+"); + + // Built lazily (not in a static initializer) so the one-time, memory-heavy classpath scan surfaces any + // failure as a normal test failure rather than a class-initialization error. + 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")); + } + + /** + * Granular feature key for a referenced type: the first two package segments under the base, so + * {@code org.apache.fineract.portfolio.charge.domain.Charge} becomes {@code portfolio.charge}. Used only to label + * violations in a readable way; it never decides whether something is allowed. + */ + private static String featureKey(String typeName) { + String prefix = BASE + "."; + if (!typeName.startsWith(prefix)) { + return typeName; + } + String[] parts = typeName.substring(prefix.length()).split("\\."); + return parts.length >= 2 ? parts[0] + "." + parts[1] : parts[0]; + } + + /** + * The {@code fineract-*} artifact a referenced type was loaded from, derived from its source URI (e.g. + * {@code .../fineract-core/build/libs/fineract-core-1.15.0-SNAPSHOT.jar!/...} -> {@code fineract-core}). Takes the + * LAST {@code /fineract-*} path segment (the deepest module dir) and strips the trailing version. Returns + * {@code (unknown-source)} if the location can't be determined. + */ + 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)"); + } + + /** + * A referenced type is foundation iff it was loaded from fineract-core or fineract-command. The decision is an + * exact match against the normalised owning artifact, not a substring of the raw URI, so siblings such as + * {@code fineract-command-jdbc} are not mistaken for foundation. If the source location is unknown, + * {@code owningArtifact} returns a sentinel not in the set, so the type surfaces as NOT foundation rather than + * silently passing. + */ + private static boolean isFoundation(JavaClass type) { + return FOUNDATION_ARTIFACTS.contains(owningArtifact(type)); + } + + /** + * The "report": prints, per accounting source type, each referenced feature package together with the artifact it + * lives in and whether that counts as foundation. Always passes; its job is to make it obvious why any given edge + * is or isn't a violation, and to locate the source type responsible. + */ + @Test + 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 (base = {}) ====", BASE); + LOG.info("-- source type -> referenced feature packages [owning artifact : status] --"); + sourceTypeToTargets.forEach((source, targets) -> LOG.info(" " + source + " -> " + targets)); + LOG.info("-- allowed (in fineract-core / fineract-command) --"); + LOG.info(" " + allowedFromCore); + LOG.info("-- VIOLATIONS (in some other fineract-* module) --"); + LOG.info(" " + violationFeatures); + } + + /** + * The check: accounting may reference types only from fineract-core and fineract-command. Any referenced type + * loaded from a different feature module is a violation, labelled by its feature package, and fixed at the source + * per the outline. + */ + @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-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); } /*** From ef40dd66389546dc006d238a0128ac1e24927328 Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Thu, 9 Jul 2026 01:57:41 +0530 Subject: [PATCH 2/7] FINERACT-2646: fix StringSplitter error prone failure in boundry test --- .../AccountingCrossFeatureBoundaryTest.java | 65 ++----------------- 1 file changed, 4 insertions(+), 61 deletions(-) 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 index 0c8899b6c77..13bb5c1c51a 100644 --- a/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java +++ b/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java @@ -20,7 +20,9 @@ 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; @@ -34,27 +36,6 @@ import org.springframework.modulith.core.ApplicationModuleDependency; import org.springframework.modulith.core.ApplicationModules; -/** - * Cross-feature boundary check for the accounting feature, using Spring Modulith. - *

- * The model is built with base {@code org.apache.fineract} so every other top-level Fineract area on the classpath is - * visible. A referenced type is "allowed foundation" when it physically lives in {@code fineract-core} or - * {@code fineract-command} — the two modules the outline lets every feature depend on. Crucially, this is decided by - * the artifact the referenced type is loaded from, not by its package name: {@code fineract-core} contains - * shared types under {@code infrastructure.*} but also under {@code organisation.*}, {@code portfolio.*}, etc. - * (DTOs/enums that were relocated to core). A package-name check would wrongly flag those; an artifact check does not. - *

- * The owning artifact is read from each type's source location (ArchUnit records where every class was imported from). - * When accounting is built, {@code fineract-core} and {@code fineract-command} are on the classpath as jars, so a core - * type resolves to a URI containing {@code fineract-core} and a command type to one containing - * {@code fineract-command}. Anything loaded from any other {@code fineract-*} artifact is a genuine cross-feature - * dependency and is reported as a violation, to be fixed at the source (move the shared type into core, or reach the - * feature via a core read-contract / command / event / by-id reference) rather than by widening an allow-list. - *

- * Because this test lives in the {@code fineract-accounting} module, it runs with only accounting and its own - * dependencies on the classpath, so it reports exactly the cross-feature edges that originate in the - * {@code fineract-accounting} jar (the work for FINERACT-2646). - */ class AccountingCrossFeatureBoundaryTest { private static final Logger LOG = LoggerFactory.getLogger(AccountingCrossFeatureBoundaryTest.class); @@ -62,20 +43,10 @@ class AccountingCrossFeatureBoundaryTest { private static final String BASE = "org.apache.fineract"; private static final String ACCOUNTING_PACKAGE = "org.apache.fineract.accounting"; - /** - * The only artifacts every feature may depend on (outline: fineract-core + fineract-command). Membership is tested - * against the referenced type's NORMALISED owning artifact (exact set match), so a type counts as foundation purely - * by living in one of these jars/modules, whatever its package happens to be. - */ private static final Set FOUNDATION_ARTIFACTS = Set.of("fineract-core", "fineract-command"); - // Match a real module path segment: "fineract-..." that begins right after a "/". This deliberately does - // NOT match the parent checkout folder (e.g. "apache-fineract-modulith"), where "fineract-modulith" is - // preceded by "apache-", not "/", and would otherwise become the first match for every type. private static final Pattern FINERACT_ARTIFACT = Pattern.compile("(?<=/)fineract-[a-z0-9-]+"); - // Built lazily (not in a static initializer) so the one-time, memory-heavy classpath scan surfaces any - // failure as a normal test failure rather than a class-initialization error. private static ApplicationModules modules; private static ApplicationModules modules() { @@ -92,26 +63,15 @@ private static ApplicationModule accountingModule() { .orElseThrow(() -> new IllegalStateException("Accounting module not found in the model")); } - /** - * Granular feature key for a referenced type: the first two package segments under the base, so - * {@code org.apache.fineract.portfolio.charge.domain.Charge} becomes {@code portfolio.charge}. Used only to label - * violations in a readable way; it never decides whether something is allowed. - */ private static String featureKey(String typeName) { String prefix = BASE + "."; if (!typeName.startsWith(prefix)) { return typeName; } - String[] parts = typeName.substring(prefix.length()).split("\\."); - return parts.length >= 2 ? parts[0] + "." + parts[1] : parts[0]; + List parts = Splitter.on('.').splitToList(typeName.substring(prefix.length())); + return parts.size() >= 2 ? parts.get(0) + "." + parts.get(1) : parts.get(0); } - /** - * The {@code fineract-*} artifact a referenced type was loaded from, derived from its source URI (e.g. - * {@code .../fineract-core/build/libs/fineract-core-1.15.0-SNAPSHOT.jar!/...} -> {@code fineract-core}). Takes the - * LAST {@code /fineract-*} path segment (the deepest module dir) and strips the trailing version. Returns - * {@code (unknown-source)} if the location can't be determined. - */ private static String owningArtifact(JavaClass type) { return type.getSource() // .map(source -> source.getUri().toString()) // @@ -126,22 +86,10 @@ private static String owningArtifact(JavaClass type) { .orElse("(unknown-source)"); } - /** - * A referenced type is foundation iff it was loaded from fineract-core or fineract-command. The decision is an - * exact match against the normalised owning artifact, not a substring of the raw URI, so siblings such as - * {@code fineract-command-jdbc} are not mistaken for foundation. If the source location is unknown, - * {@code owningArtifact} returns a sentinel not in the set, so the type surfaces as NOT foundation rather than - * silently passing. - */ private static boolean isFoundation(JavaClass type) { return FOUNDATION_ARTIFACTS.contains(owningArtifact(type)); } - /** - * The "report": prints, per accounting source type, each referenced feature package together with the artifact it - * lives in and whether that counts as foundation. Always passes; its job is to make it obvious why any given edge - * is or isn't a violation, and to locate the source type responsible. - */ @Test void printAccountingCrossFeatureDependencyReport() { Map> sourceTypeToTargets = new TreeMap<>(); @@ -174,11 +122,6 @@ void printAccountingCrossFeatureDependencyReport() { LOG.info(" " + violationFeatures); } - /** - * The check: accounting may reference types only from fineract-core and fineract-command. Any referenced type - * loaded from a different feature module is a violation, labelled by its feature package, and fixed at the source - * per the outline. - */ @Test void accountingMustNotImportOtherFeatureModules() { Set featureDependencies = new TreeSet<>(); From f13a015e8ec2dcb0a25cd1cbc5409328f6e537de Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Thu, 9 Jul 2026 12:53:36 +0530 Subject: [PATCH 3/7] FINERACT-2646: Address review feedback on accounting/charge decoupling --- fineract-accounting/dependencies.gradle | 1 - .../ProductToGLAccountMappingHelper.java | 12 ++++++ ...AccountMappingReadPlatformServiceImpl.java | 41 +++++++------------ ...avingsProductToGLAccountMappingHelper.java | 5 ++- .../ShareProductToGLAccountMappingHelper.java | 5 ++- .../AccountingCrossFeatureBoundaryTest.java | 19 ++++----- .../LoanProductToGLAccountMappingHelper.java | 5 ++- 7 files changed, 43 insertions(+), 45 deletions(-) diff --git a/fineract-accounting/dependencies.gradle b/fineract-accounting/dependencies.gradle index 310cae68907..d2fe73c0668 100644 --- a/fineract-accounting/dependencies.gradle +++ b/fineract-accounting/dependencies.gradle @@ -26,7 +26,6 @@ dependencies { // implementation(project(path: ':fineract-core')) testImplementation 'org.springframework.modulith:spring-modulith-core' - implementation(project(path: ':fineract-charge')) implementation('org.apache.avro:avro') implementation( project(path: ':fineract-avro-schemas') 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 89f8880fe0a..ab6660fd908 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 @@ -46,11 +46,13 @@ 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.exception.ResourceNotFoundException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; import org.apache.fineract.portfolio.paymenttype.exception.PaymentTypeNotFoundException; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component @@ -66,6 +68,7 @@ public class ProductToGLAccountMappingHelper { protected final GLAccountRepositoryWrapper accountRepositoryWrapper; private final PaymentTypeRepository paymentTypeRepository; private final CodeValueRepository codeValueRepository; + private final JdbcTemplate jdbcTemplate; public void saveProductToAccountMapping(final JsonElement element, final String paramName, final Long productId, final int placeHolderTypeId, final GLAccountType expectedAccountType, final PortfolioProductType portfolioProductType) { @@ -611,8 +614,17 @@ 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) { + // The accounting module no longer loads the Charge entity (it is decoupled from the charge domain), so the + // existence check that loading the entity used to provide is restored here with a lightweight JDBC lookup. + // This keeps acc_product_mapping.charge_id from persisting orphaned references without reintroducing a + // compile-time dependency on the charge module. // TODO Vishwas: Need to validate if given charge is fee or Penalty // based on input condition + final Integer chargeCount = this.jdbcTemplate.queryForObject("select count(*) from m_charge where id = ?", Integer.class, chargeId); + if (chargeCount == null || chargeCount == 0) { + throw new ResourceNotFoundException("error.msg.charge.id.invalid", "Charge with identifier " + chargeId + " does not exist", + new Object[] { chargeId }); + } GLAccount glAccount; /** 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 a0c749a08ea..1718d34aebb 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 @@ -36,8 +36,6 @@ 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.glaccount.domain.GLAccountRepository; import org.apache.fineract.accounting.producttoaccountmapping.data.AdvancedMappingToExpenseAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData; @@ -59,7 +57,6 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; private final CodeValueMapper codeValueMapper; - private final GLAccountRepository glAccountRepository; private final JdbcTemplate jdbcTemplate; @Override @@ -263,31 +260,22 @@ public List fetchPenaltyToIncomeAccountMappingsForSavin private List fetchChargeToIncomeAccountMappings(final PortfolioProductType portfolioProductType, final Long loanProductId, final boolean penalty) { // The accounting module is decoupled from the charge domain, so we cannot navigate a Charge association in - // JPQL. We read the charge -> income-account rows via JdbcTemplate instead of a JPA native query: EclipseLink + // JPQL. We read the charge -> income-account rows via JdbcTemplate rather than a JPA native query: EclipseLink // does not bind Spring Data ":name" parameters in native queries (it expects "#name"), which caused the raw // markers to reach the database. JdbcTemplate uses plain JDBC positional binding and works on all supported - // databases. Column order: [0] gl_account_id, [1] charge id, [2] charge name. The penalty flag is known from - // the method argument, so it is inlined as a boolean literal (never user input) rather than bound. - final String sql = "select apm.gl_account_id, c.id, c.name " - + "from acc_product_mapping apm inner join m_charge c on c.id = apm.charge_id " - + "where apm.product_id = ? and apm.product_type = ? and c.is_penalty = " + penalty; - final List rows = jdbcTemplate.query(sql, (rs, rowNum) -> new Object[] { rs.getLong(1), rs.getLong(2), rs.getString(3) }, - loanProductId, portfolioProductType.getValue()); - - List chargeToGLAccountMappers = rows.isEmpty() ? null : new ArrayList<>(); - for (final Object[] row : rows) { - final Long glAccountId = (Long) row[0]; - final Long chargeId = (Long) row[1]; - final String chargeName = (String) row[2]; - final GLAccount glAccount = glAccountRepository.findById(glAccountId).orElseThrow(); - final GLAccountData gLAccountData = new GLAccountData().setId(glAccount.getId()).setName(glAccount.getName()) - .setGlCode(glAccount.getGlCode()); - final ChargeData chargeData = ChargeData.builder().id(chargeId).name(chargeName).penalty(penalty).build(); - final ChargeToGLAccountMapper chargeToGLAccountMapper = new ChargeToGLAccountMapper().setCharge(chargeData) - .setIncomeAccount(gLAccountData); - chargeToGLAccountMappers.add(chargeToGLAccountMapper); - } - return chargeToGLAccountMappers; + // databases. The GL account fields are selected via a join on acc_gl_account so we avoid a per-row lookup. + // Column order: [0] gl_account_id, [1] gl_account_name, [2] gl_code, [3] charge_id, [4] charge_name. + final String sql = "select ga.id, ga.name, ga.gl_code, c.id, c.name " + "from acc_product_mapping apm " + + "inner join m_charge c on c.id = apm.charge_id " + "inner join acc_gl_account ga on ga.id = apm.gl_account_id " + + "where apm.product_id = ? and apm.product_type = ? and c.is_penalty = ?"; + final List chargeToGLAccountMappers = jdbcTemplate.query(sql, (rs, rowNum) -> { + final GLAccountData gLAccountData = new GLAccountData().setId(rs.getLong(1)).setName(rs.getString(2)) + .setGlCode(rs.getString(3)); + final ChargeData chargeData = ChargeData.builder().id(rs.getLong(4)).name(rs.getString(5)).penalty(penalty).build(); + return new ChargeToGLAccountMapper().setCharge(chargeData).setIncomeAccount(gLAccountData); + }, loanProductId, portfolioProductType.getValue(), penalty); + + return chargeToGLAccountMappers.isEmpty() ? null : chargeToGLAccountMappers; } private List fetchChargeOffReasonMappings(final PortfolioProductType portfolioProductType, @@ -328,7 +316,6 @@ private List fetchClassificationMappings(final Po : productToGLAccountMappingRepository.findAllBuyDownFeeClassificationsMappings(loanProductId, portfolioProductType.getValue()); - productToGLAccountMappingRepository.findAllChargeOffReasonsMappings(loanProductId, portfolioProductType.getValue()); List classificationToGLAccountMappers = mappings.isEmpty() ? null : new ArrayList<>(); for (final ProductToGLAccountMapping mapping : mappings) { final Long glAccountId = mapping.getGlAccount().getId(); 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 3826e847c01..ab0c3700fb4 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 @@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component @@ -42,9 +43,9 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa public SavingsProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, final GLAccountRepositoryWrapper accountRepositoryWrapper, final PaymentTypeRepository paymentTypeRepository, - final CodeValueRepository codeValueRepository) { + final CodeValueRepository codeValueRepository, final JdbcTemplate jdbcTemplate) { super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, accountRepositoryWrapper, paymentTypeRepository, - codeValueRepository); + codeValueRepository, jdbcTemplate); } public void saveSavingsToAssetAccountMapping(final JsonElement element, final String paramName, final Long productId, 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 7fb3e5f58cc..f6da93d5cd3 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 @@ -33,6 +33,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component @@ -41,9 +42,9 @@ public class ShareProductToGLAccountMappingHelper extends ProductToGLAccountMapp public ShareProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, final GLAccountRepositoryWrapper accountRepositoryWrapper, final PaymentTypeRepository paymentTypeRepository, - final CodeValueRepository codeValueRepository) { + final CodeValueRepository codeValueRepository, final JdbcTemplate jdbcTemplate) { super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, accountRepositoryWrapper, paymentTypeRepository, - codeValueRepository); + codeValueRepository, jdbcTemplate); } /*** 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 index 13bb5c1c51a..8212b487697 100644 --- a/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java +++ b/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java @@ -73,17 +73,14 @@ private static String featureKey(String typeName) { } 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)"); + 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) { 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 1db5f3e7ede..8a70d891586 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 @@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component @@ -47,9 +48,9 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi public LoanProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, final GLAccountRepositoryWrapper accountRepositoryWrapper, final PaymentTypeRepository paymentTypeRepository, - final CodeValueRepository codeValueRepository) { + final CodeValueRepository codeValueRepository, final JdbcTemplate jdbcTemplate) { super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, accountRepositoryWrapper, paymentTypeRepository, - codeValueRepository); + codeValueRepository, jdbcTemplate); } /*** From cf2ddee60b2ac7cee1bb29b0df05c3e2fc503902 Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Thu, 9 Jul 2026 17:06:12 +0530 Subject: [PATCH 4/7] FINERACT-2646: fix SLF4J SpotBugs violations in accounting boundary test --- fineract-accounting/dependencies.gradle | 1 + .../AccountingCrossFeatureBoundaryTest.java | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/fineract-accounting/dependencies.gradle b/fineract-accounting/dependencies.gradle index d2fe73c0668..ad075c1adc2 100644 --- a/fineract-accounting/dependencies.gradle +++ b/fineract-accounting/dependencies.gradle @@ -26,6 +26,7 @@ dependencies { // implementation(project(path: ':fineract-core')) 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/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java b/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java index 8212b487697..172c6ab57ab 100644 --- a/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java +++ b/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java @@ -110,13 +110,19 @@ void printAccountingCrossFeatureDependencyReport() { } }); - LOG.info("==== Accounting cross-feature dependency report (base = {}) ====", BASE); + 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 + " -> " + targets)); - LOG.info("-- allowed (in fineract-core / fineract-command) --"); - LOG.info(" " + allowedFromCore); - LOG.info("-- VIOLATIONS (in some other fineract-* module) --"); - LOG.info(" " + violationFeatures); + + 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 From c0cfbdf58d45f9ab89b4622a47804bf74b521604 Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Fri, 10 Jul 2026 01:00:57 +0530 Subject: [PATCH 5/7] FINERACT-2646: Auto enable report test based on the flag --- .../fineract/accounting/AccountingCrossFeatureBoundaryTest.java | 2 ++ 1 file changed, 2 insertions(+) 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 index 172c6ab57ab..57af11f494c 100644 --- a/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java +++ b/fineract-accounting/src/test/java/org/apache/fineract/accounting/AccountingCrossFeatureBoundaryTest.java @@ -30,6 +30,7 @@ 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; @@ -88,6 +89,7 @@ private static boolean isFoundation(JavaClass type) { } @Test + @EnabledIfSystemProperty(named = "accounting.boundary.report", matches = "true") void printAccountingCrossFeatureDependencyReport() { Map> sourceTypeToTargets = new TreeMap<>(); Set violationFeatures = new TreeSet<>(); From c12cd0f2dcb203f9630e7ec19125104a2c13a1cd Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Wed, 15 Jul 2026 16:50:57 +0530 Subject: [PATCH 6/7] FINERACT-2646: Decouple accounting from charge via read contract --- .../org.apache.fineract.dependencies.gradle | 1 + fineract-accounting/dependencies.gradle | 1 - .../service/AccountingChargeReadService.java | 28 +++++++++++ .../ProductToGLAccountMappingHelper.java | 15 ------ ...AccountMappingReadPlatformServiceImpl.java | 43 +++++++++-------- ...avingsProductToGLAccountMappingHelper.java | 5 +- .../ShareProductToGLAccountMappingHelper.java | 5 +- ...anProductAdvancedAccountingReadHelper.java | 41 ++++++++-------- fineract-charge/dependencies.gradle | 1 + .../AccountingChargeReadServiceImpl.java | 47 +++++++++++++++++++ .../LoanProductToGLAccountMappingHelper.java | 5 +- ...ccountMappingWritePlatformServiceImpl.java | 41 ++++++++++++++-- 12 files changed, 162 insertions(+), 71 deletions(-) create mode 100644 fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/AccountingChargeReadService.java create mode 100644 fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/AccountingChargeReadServiceImpl.java 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/dependencies.gradle b/fineract-accounting/dependencies.gradle index ad075c1adc2..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. */ -ext['spring-modulith.version'] = '1.4.11' 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. 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 ab6660fd908..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 @@ -46,13 +46,11 @@ 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.exception.ResourceNotFoundException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; import org.apache.fineract.portfolio.paymenttype.exception.PaymentTypeNotFoundException; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component @@ -68,7 +66,6 @@ public class ProductToGLAccountMappingHelper { protected final GLAccountRepositoryWrapper accountRepositoryWrapper; private final PaymentTypeRepository paymentTypeRepository; private final CodeValueRepository codeValueRepository; - private final JdbcTemplate jdbcTemplate; public void saveProductToAccountMapping(final JsonElement element, final String paramName, final Long productId, final int placeHolderTypeId, final GLAccountType expectedAccountType, final PortfolioProductType portfolioProductType) { @@ -614,18 +611,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) { - // The accounting module no longer loads the Charge entity (it is decoupled from the charge domain), so the - // existence check that loading the entity used to provide is restored here with a lightweight JDBC lookup. - // This keeps acc_product_mapping.charge_id from persisting orphaned references without reintroducing a - // compile-time dependency on the charge module. - // TODO Vishwas: Need to validate if given charge is fee or Penalty - // based on input condition - final Integer chargeCount = this.jdbcTemplate.queryForObject("select count(*) from m_charge where id = ?", Integer.class, chargeId); - if (chargeCount == null || chargeCount == 0) { - throw new ResourceNotFoundException("error.msg.charge.id.invalid", "Charge with identifier " + chargeId + " does not exist", - new Object[] { chargeId }); - } - GLAccount glAccount; /** * Both CASH and Accrual placeholders have the same value for income from Interest and penalties 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 1718d34aebb..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; @@ -47,7 +50,6 @@ import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @Slf4j @@ -57,7 +59,7 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; private final CodeValueMapper codeValueMapper; - private final JdbcTemplate jdbcTemplate; + private final AccountingChargeReadService accountingChargeReadService; @Override public Map fetchAccountMappingDetailsForLoanProduct(final Long loanProductId, final Integer accountingType) { @@ -259,23 +261,26 @@ public List fetchPenaltyToIncomeAccountMappingsForSavin private List fetchChargeToIncomeAccountMappings(final PortfolioProductType portfolioProductType, final Long loanProductId, final boolean penalty) { - // The accounting module is decoupled from the charge domain, so we cannot navigate a Charge association in - // JPQL. We read the charge -> income-account rows via JdbcTemplate rather than a JPA native query: EclipseLink - // does not bind Spring Data ":name" parameters in native queries (it expects "#name"), which caused the raw - // markers to reach the database. JdbcTemplate uses plain JDBC positional binding and works on all supported - // databases. The GL account fields are selected via a join on acc_gl_account so we avoid a per-row lookup. - // Column order: [0] gl_account_id, [1] gl_account_name, [2] gl_code, [3] charge_id, [4] charge_name. - final String sql = "select ga.id, ga.name, ga.gl_code, c.id, c.name " + "from acc_product_mapping apm " - + "inner join m_charge c on c.id = apm.charge_id " + "inner join acc_gl_account ga on ga.id = apm.gl_account_id " - + "where apm.product_id = ? and apm.product_type = ? and c.is_penalty = ?"; - final List chargeToGLAccountMappers = jdbcTemplate.query(sql, (rs, rowNum) -> { - final GLAccountData gLAccountData = new GLAccountData().setId(rs.getLong(1)).setName(rs.getString(2)) - .setGlCode(rs.getString(3)); - final ChargeData chargeData = ChargeData.builder().id(rs.getLong(4)).name(rs.getString(5)).penalty(penalty).build(); - return new ChargeToGLAccountMapper().setCharge(chargeData).setIncomeAccount(gLAccountData); - }, loanProductId, portfolioProductType.getValue(), penalty); - - return chargeToGLAccountMappers.isEmpty() ? null : chargeToGLAccountMappers; + final List mappings = penalty + ? 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())); + + final List chargeToGLAccountMappers = new ArrayList<>(); + for (final ProductToGLAccountMapping mapping : mappings) { + 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; } private List fetchChargeOffReasonMappings(final PortfolioProductType portfolioProductType, 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 ab0c3700fb4..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 @@ -34,7 +34,6 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component @@ -43,9 +42,9 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa public SavingsProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, final GLAccountRepositoryWrapper accountRepositoryWrapper, final PaymentTypeRepository paymentTypeRepository, - final CodeValueRepository codeValueRepository, final JdbcTemplate jdbcTemplate) { + final CodeValueRepository codeValueRepository) { super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, accountRepositoryWrapper, paymentTypeRepository, - codeValueRepository, jdbcTemplate); + codeValueRepository); } public void saveSavingsToAssetAccountMapping(final JsonElement element, final String paramName, final Long productId, 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 f6da93d5cd3..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 @@ -33,7 +33,6 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component @@ -42,9 +41,9 @@ public class ShareProductToGLAccountMappingHelper extends ProductToGLAccountMapp public ShareProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, final GLAccountRepositoryWrapper accountRepositoryWrapper, final PaymentTypeRepository paymentTypeRepository, - final CodeValueRepository codeValueRepository, final JdbcTemplate jdbcTemplate) { + final CodeValueRepository codeValueRepository) { super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, accountRepositoryWrapper, paymentTypeRepository, - codeValueRepository, jdbcTemplate); + 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 70748520699..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,10 +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.glaccount.domain.GLAccountRepository; import org.apache.fineract.accounting.producttoaccountmapping.data.AdvancedMappingToExpenseAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; @@ -34,7 +36,6 @@ import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component @@ -43,8 +44,7 @@ public class WorkingCapitalLoanProductAdvancedAccountingReadHelper { private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; private final CodeValueMapper codeValueMapper; - private final GLAccountRepository glAccountRepository; - private final JdbcTemplate jdbcTemplate; + private final AccountingChargeReadService accountingChargeReadService; public List fetchPaymentTypeToFundSourceMappings(final Long wcLoanProductId) { final List mappings = productToGLAccountMappingRepository.findAllPaymentTypeMappings(wcLoanProductId, @@ -79,29 +79,26 @@ public List fetchWriteOffReasonMappings(fin } private List fetchChargeToIncomeMappings(final Long wcLoanProductId, final boolean penalty) { - // The charge name lives on m_charge, not on ProductToGLAccountMapping (which only stores charge_id), and the - // accounting module is decoupled from the charge domain, so we read the charge -> income-account rows via - // JdbcTemplate rather than a JPA native query (EclipseLink does not bind Spring Data ":name" markers in native - // queries). Column order: [0] gl_account_id, [1] charge id, [2] charge name. The penalty flag is known from the - // method argument, so it is inlined as a boolean literal (never user input) rather than bound. - final String sql = "select apm.gl_account_id, c.id, c.name " - + "from acc_product_mapping apm inner join m_charge c on c.id = apm.charge_id " - + "where apm.product_id = ? and apm.product_type = ? and c.is_penalty = " + penalty; - final List rows = jdbcTemplate.query(sql, (rs, rowNum) -> new Object[] { rs.getLong(1), rs.getLong(2), rs.getString(3) }, - wcLoanProductId, PortfolioProductType.WORKING_CAPITAL_LOAN.getValue()); + final int productType = PortfolioProductType.WORKING_CAPITAL_LOAN.getValue(); + final List mappings = penalty + ? 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 Object[] row : rows) { - final Long glAccountId = (Long) row[0]; - final Long chargeId = (Long) row[1]; - final String chargeName = (String) row[2]; - final GLAccount glAccount = glAccountRepository.findById(glAccountId).orElseThrow(); + for (final ProductToGLAccountMapping mapping : mappings) { + final GLAccount glAccount = mapping.getGlAccount(); final GLAccountData gLAccountData = new GLAccountData().setId(glAccount.getId()).setName(glAccount.getName()) .setGlCode(glAccount.getGlCode()); - final ChargeData chargeData = ChargeData.builder().id(chargeId).name(chargeName).penalty(penalty).build(); - result.add(new ChargeToGLAccountMapper().setCharge(chargeData).setIncomeAccount(gLAccountData)); + 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-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 8a70d891586..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 @@ -39,7 +39,6 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; @Component @@ -48,9 +47,9 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi public LoanProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository, final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper, final GLAccountRepositoryWrapper accountRepositoryWrapper, final PaymentTypeRepository paymentTypeRepository, - final CodeValueRepository codeValueRepository, final JdbcTemplate jdbcTemplate) { + final CodeValueRepository codeValueRepository) { super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, accountRepositoryWrapper, paymentTypeRepository, - codeValueRepository, jdbcTemplate); + 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..65f69589abc 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 @@ -18,14 +18,10 @@ */ package org.apache.fineract.accounting.productaccountmapping.service; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountingRuleParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.isDormancyTrackingActiveParamName; - -import com.google.gson.JsonElement; import java.util.HashMap; import java.util.Locale; import java.util.Map; -import lombok.RequiredArgsConstructor; + import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan; import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan; @@ -41,11 +37,19 @@ 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 static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountingRuleParamName; +import static org.apache.fineract.portfolio.savings.SavingsApiConstants.isDormancyTrackingActiveParamName; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + +import lombok.RequiredArgsConstructor; + @Service @RequiredArgsConstructor public class ProductToGLAccountMappingWritePlatformServiceImpl implements ProductToGLAccountMappingWritePlatformService { @@ -55,11 +59,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 +317,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 +361,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 +423,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 +463,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 +495,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 +503,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()); + } + } + } } From a47b44a5b75fd5741e6633759be9bd5a742f09d4 Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Wed, 15 Jul 2026 17:28:45 +0530 Subject: [PATCH 7/7] FINERACT-2646: Fix Spotless import ordering in ProductToGLAccountMappingWritePlatformServiceImpl --- ...ToGLAccountMappingWritePlatformServiceImpl.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 65f69589abc..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 @@ -18,10 +18,15 @@ */ package org.apache.fineract.accounting.productaccountmapping.service; +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; import java.util.Map; - +import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan; import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan; @@ -40,16 +45,9 @@ import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.savings.DepositAccountType; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountingRuleParamName; -import static org.apache.fineract.portfolio.savings.SavingsApiConstants.isDormancyTrackingActiveParamName; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; - -import lombok.RequiredArgsConstructor; - @Service @RequiredArgsConstructor public class ProductToGLAccountMappingWritePlatformServiceImpl implements ProductToGLAccountMappingWritePlatformService {