From e89a2320f47be88524f4cf84034a26272cdf0baf Mon Sep 17 00:00:00 2001 From: "adam.magyari" Date: Wed, 11 Dec 2024 08:56:15 +0100 Subject: [PATCH 1/2] FINERACT-1806: fix ProductToGLAccountMapping --- .../domain/ProductToGLAccountMapping.java | 10 +- .../ProductToGLAccountMappingRepository.java | 16 +- .../ProductToGLAccountMappingHelper.java | 10 +- ...AccountMappingReadPlatformServiceImpl.java | 232 +++++------------- .../api/LoanProductsApiResourceSwagger.java | 51 +++- .../db/changelog/tenant/changelog-tenant.xml | 1 + ...d_acc_product_mapping_product_id_index.xml | 33 +++ ...oanProductChargeOffReasonMappingsTest.java | 20 +- 8 files changed, 179 insertions(+), 194 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0160_add_acc_product_mapping_product_id_index.xml 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 43680cb5b75..5e953ff27be 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 @@ -29,6 +29,7 @@ import lombok.Setter; import lombok.experimental.Accessors; 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; @@ -63,13 +64,14 @@ public class ProductToGLAccountMapping extends AbstractPersistableCustom { @Column(name = "financial_account_type", nullable = true) private int financialAccountType; - @Column(name = "charge_off_reason_id", nullable = true) - private Long chargeOffReasonId; + @ManyToOne + @JoinColumn(name = "charge_off_reason_id", nullable = true) + private CodeValue chargeOffReason; public static ProductToGLAccountMapping createNew(final GLAccount glAccount, final Long productId, final int productType, - final int financialAccountType, final Long chargeOffReasonId) { + final int financialAccountType, final CodeValue chargeOffReasonId) { return new ProductToGLAccountMapping().setGlAccount(glAccount).setProductId(productId).setProductType(productType) - .setFinancialAccountType(financialAccountType).setChargeOffReasonId(chargeOffReasonId); + .setFinancialAccountType(financialAccountType).setChargeOffReason(chargeOffReasonId); } } diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java index acd32ad33a6..0dd2b6fb840 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java @@ -35,7 +35,7 @@ ProductToGLAccountMapping findProductIdAndProductTypeAndFinancialAccountTypeAndC @Param("productType") int productType, @Param("financialAccountType") int financialAccountType, @Param("chargeId") Long ChargeId); - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReasonId is NULL") + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReason is NULL") ProductToGLAccountMapping findCoreProductToFinAccountMapping(@Param("productId") Long productId, @Param("productType") int productType, @Param("financialAccountType") int financialAccountType); @@ -62,10 +62,20 @@ List findAllPenaltyToIncomeAccountMappings(@Param("pr List findByProductIdAndProductType(Long productId, int productType); - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.chargeOffReasonId is not NULL") + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.chargeOffReason is not NULL") List findAllChargesOffReasonsMappings(@Param("productId") Long productId, @Param("productType") int productType); - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.chargeOffReasonId =:chargeOffReasonId") + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.chargeOffReason.id =:chargeOffReasonId") ProductToGLAccountMapping findChargesOffReasonMappingById(@Param("chargeOffReasonId") Integer chargeOffReasonId); + + List findAllByProductIdAndProductTypeAndPaymentTypeIsNullAndChargeIsNull(Long productId, + Integer productType); + + List findAllByProductIdAndProductTypeAndChargeOffReasonIdIsNotNull(Long productId, Integer productType); + + List findAllByProductIdAndProductTypeAndPaymentTypeIsNotNull(Long productId, Integer productType); + + List findAllByProductIdAndProductTypeAndCharge_PenaltyAndCharge_IdIsNotNull(Long productId, + Integer productType, boolean isPenalty); } 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 5c01b16706b..34b9e06d762 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 @@ -414,7 +414,7 @@ public void updateChargeOffReasonToGLAccountMappings(final JsonCommand command, this.accountMappingRepository.deleteAllInBatch(existingChargeOffReasonToGLAccountMappings); } else { for (final ProductToGLAccountMapping existingChargeOffReasonToGLAccountMapping : existingChargeOffReasonToGLAccountMappings) { - final Long currentChargeOffReasonId = existingChargeOffReasonToGLAccountMapping.getChargeOffReasonId(); + final Long currentChargeOffReasonId = existingChargeOffReasonToGLAccountMapping.getChargeOffReason().getId(); if (currentChargeOffReasonId != null) { existingChargeOffReasons.add(currentChargeOffReasonId); // update existing mappings (if required) @@ -496,12 +496,14 @@ private void saveChargeOffReasonToExpenseMapping(final Long productId, final Lon final boolean reasonMappingExists = this.accountMappingRepository .findAllChargesOffReasonsMappings(productId, portfolioProductType.getValue()).stream() - .anyMatch(mapping -> mapping.getChargeOffReasonId().equals(reasonId)); + .anyMatch(mapping -> mapping.getChargeOffReason().getId().equals(reasonId)); - if (glAccount.isPresent() && !reasonMappingExists) { + final Optional codeValueOptional = codeValueRepository.findById(reasonId); + + if (glAccount.isPresent() && !reasonMappingExists && codeValueOptional.isPresent()) { final ProductToGLAccountMapping accountMapping = new ProductToGLAccountMapping().setGlAccount(glAccount.get()) .setProductId(productId).setProductType(portfolioProductType.getValue()) - .setFinancialAccountType(CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue()).setChargeOffReasonId(reasonId); + .setFinancialAccountType(CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue()).setChargeOffReason(codeValueOptional.get()); 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 1803d12f1dc..fd3cbfd8543 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 @@ -18,8 +18,6 @@ */ package org.apache.fineract.accounting.producttoaccountmapping.service; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -40,13 +38,13 @@ import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; +import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping; +import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; import org.apache.fineract.infrastructure.codes.data.CodeValueData; -import org.apache.fineract.infrastructure.core.domain.JdbcSupport; 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.jdbc.core.RowMapper; import org.springframework.stereotype.Service; @Slf4j @@ -56,86 +54,24 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product private final JdbcTemplate jdbcTemplate; - private static final class ProductToGLAccountMappingMapper implements RowMapper> { - - public String schema() { - return " mapping.id as id, mapping.gl_account_id as glAccountId,glaccount.name as name,glaccount.gl_code as code," - + " mapping.product_id as productId, mapping.product_type as productType,mapping.financial_account_type as financialAccountType, " - + " mapping.payment_type as paymentTypeId,pt.value as paymentTypeValue, mapping.charge_id as chargeId, mapping.charge_off_reason_id as chargeOffReasonId, charge.is_penalty as penalty, " - + " charge.name as chargeName, codeValue.code_value as codeValueName, codeValue.code_description as codeDescription, codeValue.order_position as orderPosition, codeValue.is_active as isActive, codeValue.is_mandatory as isMandatory " - + " from acc_product_mapping mapping left join m_charge charge on mapping.charge_id=charge.id " - + " left join acc_gl_account as glaccount on mapping.gl_account_id = glaccount.id " - + " left join m_code_value as codeValue on mapping.charge_off_reason_id = codeValue.id " - + " left join m_payment_type pt on mapping.payment_type=pt.id" + " where mapping.product_type= ? "; - } - - @Override - public Map mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final Long glAccountId = rs.getLong("glAccountId"); - final Long productId = rs.getLong("productId"); - final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentTypeId"); - final Long chargeId = rs.getLong("chargeId"); - final Integer productType = rs.getInt("productType"); - final String paymentTypeValue = rs.getString("paymentTypeValue"); - final Integer financialAccountType = rs.getInt("financialAccountType"); - final String glAccountName = rs.getString("name"); - final String glCode = rs.getString("code"); - final String chargeName = rs.getString("chargeName"); - final Boolean penalty = rs.getBoolean("penalty"); - final Integer chargeOffReasonId = rs.getInt("chargeOffReasonId"); - final String codeValue = rs.getString("codeValueName"); - final String codeDescription = rs.getString("codeDescription"); - final Integer orderPosition = rs.getInt("orderPosition"); - final Boolean isActive = rs.getBoolean("isActive"); - final Boolean isMandatory = rs.getBoolean("isMandatory"); - - final Map loanProductToGLAccountMap = new LinkedHashMap<>(5); - loanProductToGLAccountMap.put("id", id); - loanProductToGLAccountMap.put("glAccountId", glAccountId); - loanProductToGLAccountMap.put("productId", productId); - loanProductToGLAccountMap.put("productType", productType); - loanProductToGLAccountMap.put("financialAccountType", financialAccountType); - loanProductToGLAccountMap.put("paymentTypeId", paymentTypeId); - loanProductToGLAccountMap.put("paymentTypeValue", paymentTypeValue); - loanProductToGLAccountMap.put("chargeId", chargeId); - loanProductToGLAccountMap.put("chargeName", chargeName); - loanProductToGLAccountMap.put("penalty", penalty); - loanProductToGLAccountMap.put("glAccountName", glAccountName); - loanProductToGLAccountMap.put("glCode", glCode); - loanProductToGLAccountMap.put("chargeOffReasonId", chargeOffReasonId); - loanProductToGLAccountMap.put("codeValue", codeValue); - loanProductToGLAccountMap.put("codeDescription", codeDescription); - loanProductToGLAccountMap.put("orderPosition", orderPosition); - loanProductToGLAccountMap.put("isActive", isActive); - loanProductToGLAccountMap.put("isMandatory", isMandatory); - return loanProductToGLAccountMap; - } - } + private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; @Override public Map fetchAccountMappingDetailsForLoanProduct(final Long loanProductId, final Integer accountingType) { final Map accountMappingDetails = new LinkedHashMap<>(8); - final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper(); - final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null and mapping.charge_id is null"; - - final List> listOfProductToGLAccountMaps = this.jdbcTemplate.query(sql, rm, // NOSONAR - new Object[] { PortfolioProductType.LOAN.getValue(), loanProductId }); + final List mappings = productToGLAccountMappingRepository + .findAllByProductIdAndProductTypeAndPaymentTypeIsNullAndChargeIsNull(loanProductId, PortfolioProductType.LOAN.getValue()); if (AccountingValidations.isCashBasedAccounting(accountingType)) { - for (final Map productToGLAccountMap : listOfProductToGLAccountMaps) { + for (final ProductToGLAccountMapping mapping : mappings) { - final Integer financialAccountType = (Integer) productToGLAccountMap.get("financialAccountType"); - final CashAccountsForLoan glAccountForLoan = CashAccountsForLoan.fromInt(financialAccountType); + final CashAccountsForLoan glAccountForLoan = CashAccountsForLoan.fromInt(mapping.getFinancialAccountType()); - final Long glAccountId = (Long) productToGLAccountMap.get("glAccountId"); - final String glAccountName = (String) productToGLAccountMap.get("glAccountName"); - final String glCode = (String) productToGLAccountMap.get("glCode"); - final GLAccountData gLAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); + final GLAccountData gLAccountData = new GLAccountData().setId(mapping.getGlAccount().getId()) + .setName(mapping.getGlAccount().getName()).setGlCode(mapping.getGlAccount().getGlCode()); if (glAccountForLoan.equals(CashAccountsForLoan.FUND_SOURCE)) { accountMappingDetails.put(LoanProductAccountingDataParams.FUND_SOURCE.getValue(), gLAccountData); @@ -181,14 +117,11 @@ public Map fetchAccountMappingDetailsForLoanProduct(final Long l } else if (AccountingValidations.isAccrualBasedAccounting(accountingType) || AccountingValidations.isUpfrontAccrualAccounting(accountingType)) { - for (final Map productToGLAccountMap : listOfProductToGLAccountMaps) { - final Integer financialAccountType = (Integer) productToGLAccountMap.get("financialAccountType"); - final AccrualAccountsForLoan glAccountForLoan = AccrualAccountsForLoan.fromInt(financialAccountType); + for (ProductToGLAccountMapping mapping : mappings) { + final AccrualAccountsForLoan glAccountForLoan = AccrualAccountsForLoan.fromInt(mapping.getFinancialAccountType()); - final Long glAccountId = (Long) productToGLAccountMap.get("glAccountId"); - final String glAccountName = (String) productToGLAccountMap.get("glAccountName"); - final String glCode = (String) productToGLAccountMap.get("glCode"); - final GLAccountData gLAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); + final GLAccountData gLAccountData = new GLAccountData().setId(mapping.getGlAccount().getId()) + .setName(mapping.getGlAccount().getName()).setGlCode(mapping.getGlAccount().getGlCode()); if (glAccountForLoan.equals(AccrualAccountsForLoan.FUND_SOURCE)) { accountMappingDetails.put(LoanProductAccountingDataParams.FUND_SOURCE.getValue(), gLAccountData); @@ -244,18 +177,17 @@ public Map fetchAccountMappingDetailsForLoanProduct(final Long l @Override public Map fetchAccountMappingDetailsForSavingsProduct(final Long savingsProductId, final Integer accountingType) { - final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper(); - final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null and mapping.charge_id is null "; - final List> listOfProductToGLAccountMaps = this.jdbcTemplate.query(sql, rm, // NOSONAR - new Object[] { PortfolioProductType.SAVING.getValue(), savingsProductId }); + final List mappings = productToGLAccountMappingRepository + .findAllByProductIdAndProductTypeAndPaymentTypeIsNullAndChargeIsNull(savingsProductId, + PortfolioProductType.SAVING.getValue()); Map accountMappingDetails = null; if (AccountingValidations.isCashBasedAccounting(accountingType)) { - accountMappingDetails = setCashSavingsProductToGLAccountMaps(listOfProductToGLAccountMaps); + accountMappingDetails = setCashSavingsProductToGLAccountMaps(mappings); } else if (AccountingValidations.isAccrualPeriodicBasedAccounting(accountingType)) { - accountMappingDetails = setAccrualPeriodicSavingsProductToGLAccountMaps(listOfProductToGLAccountMaps); + accountMappingDetails = setAccrualPeriodicSavingsProductToGLAccountMaps(mappings); } @@ -278,25 +210,18 @@ public List fetchPaymentTypeToFundSourceMappingsFo */ private List fetchPaymentTypeToFundSourceMappings(final PortfolioProductType portfolioProductType, final Long loanProductId) { - final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper(); - final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is not null"; - - final List> paymentTypeToFundSourceMappingsList = this.jdbcTemplate.query(sql, rm, // NOSONAR - new Object[] { portfolioProductType.getValue(), loanProductId }); + final List mappings = productToGLAccountMappingRepository + .findAllByProductIdAndProductTypeAndPaymentTypeIsNotNull(loanProductId, portfolioProductType.getValue()); List paymentTypeToGLAccountMappers = null; - for (final Map productToGLAccountMap : paymentTypeToFundSourceMappingsList) { + for (final ProductToGLAccountMapping mapping : mappings) { if (paymentTypeToGLAccountMappers == null) { paymentTypeToGLAccountMappers = new ArrayList<>(); } - final Long paymentTypeId = (Long) productToGLAccountMap.get("paymentTypeId"); - final String paymentTypeValue = (String) productToGLAccountMap.get("paymentTypeValue"); - final PaymentTypeData paymentTypeData = PaymentTypeData.instance(paymentTypeId, paymentTypeValue); - final Long glAccountId = (Long) productToGLAccountMap.get("glAccountId"); - final String glAccountName = (String) productToGLAccountMap.get("glAccountName"); - final String glCode = (String) productToGLAccountMap.get("glCode"); - - final GLAccountData gLAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); + final PaymentTypeData paymentTypeData = PaymentTypeData.instance(mapping.getPaymentType().getId(), + mapping.getPaymentType().getName()); + final GLAccountData gLAccountData = new GLAccountData().setId(mapping.getGlAccount().getId()) + .setName(mapping.getGlAccount().getName()).setGlCode(mapping.getGlAccount().getGlCode()); final PaymentTypeToGLAccountMapper paymentTypeToGLAccountMapper = new PaymentTypeToGLAccountMapper() .setPaymentType(paymentTypeData).setFundSourceAccount(gLAccountData); @@ -327,29 +252,18 @@ public List fetchPenaltyToIncomeAccountMappingsForSavin private List fetchChargeToIncomeAccountMappings(final PortfolioProductType portfolioProductType, final Long loanProductId, final boolean penalty) { - final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper(); - String sql = "select " + rm.schema() + " and product_id = ? and mapping.charge_id is not null and charge.is_penalty="; - if (penalty) { - sql = sql + " true"; - } else { - sql = sql + " false"; - } - - final List> chargeToFundSourceMappingsList = this.jdbcTemplate.query(sql, rm, // NOSONAR - new Object[] { portfolioProductType.getValue(), loanProductId }); + final List mappings = productToGLAccountMappingRepository + .findAllByProductIdAndProductTypeAndCharge_PenaltyAndCharge_IdIsNotNull(loanProductId, portfolioProductType.getValue(), + penalty); List chargeToGLAccountMappers = null; - for (final Map chargeToIncomeAccountMap : chargeToFundSourceMappingsList) { + for (final ProductToGLAccountMapping mapping : mappings) { if (chargeToGLAccountMappers == null) { chargeToGLAccountMappers = new ArrayList<>(); } - final Long glAccountId = (Long) chargeToIncomeAccountMap.get("glAccountId"); - final String glAccountName = (String) chargeToIncomeAccountMap.get("glAccountName"); - final String glCode = (String) chargeToIncomeAccountMap.get("glCode"); - final GLAccountData gLAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); - final Long chargeId = (Long) chargeToIncomeAccountMap.get("chargeId"); - final String chargeName = (String) chargeToIncomeAccountMap.get("chargeName"); - final Boolean penalty1 = (Boolean) chargeToIncomeAccountMap.get("penalty"); - final ChargeData chargeData = ChargeData.builder().id(chargeId).name(chargeName).penalty(penalty1).build(); + final GLAccountData gLAccountData = new GLAccountData().setId(mapping.getGlAccount().getId()) + .setName(mapping.getGlAccount().getName()).setGlCode(mapping.getGlAccount().getGlCode()); + final ChargeData chargeData = ChargeData.builder().id(mapping.getCharge().getId()).name(mapping.getCharge().getName()) + .penalty(mapping.getCharge().isPenalty()).build(); final ChargeToGLAccountMapper chargeToGLAccountMapper = new ChargeToGLAccountMapper().setCharge(chargeData) .setIncomeAccount(gLAccountData); chargeToGLAccountMappers.add(chargeToGLAccountMapper); @@ -359,28 +273,25 @@ private List fetchChargeToIncomeAccountMappings(final P private List fetchChargeOffReasonMappings(final PortfolioProductType portfolioProductType, final Long loanProductId) { - final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper(); - String sql = "select " + rm.schema() + " and product_id = ? and mapping.charge_off_reason_id is not null"; - - final List> chargeOffReasonMappingsList = this.jdbcTemplate.query(sql, rm, // NOSONAR - new Object[] { portfolioProductType.getValue(), loanProductId }); + final List mappings = productToGLAccountMappingRepository + .findAllByProductIdAndProductTypeAndChargeOffReasonIdIsNotNull(loanProductId, portfolioProductType.getValue()); List chargeOffReasonToGLAccountMappers = null; - for (final Map chargeOffReasonMap : chargeOffReasonMappingsList) { + for (final ProductToGLAccountMapping mapping : mappings) { if (chargeOffReasonToGLAccountMappers == null) { chargeOffReasonToGLAccountMappers = new ArrayList<>(); } - final Long glAccountId = (Long) chargeOffReasonMap.get("glAccountId"); - final String glAccountName = (String) chargeOffReasonMap.get("glAccountName"); - final String glCode = (String) chargeOffReasonMap.get("glCode"); + final Long glAccountId = mapping.getGlAccount().getId(); + final String glAccountName = mapping.getGlAccount().getName(); + final String glCode = mapping.getGlAccount().getGlCode(); final GLAccountData chargeOffExpenseAccount = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); - final Integer chargeOffReasonId = (Integer) chargeOffReasonMap.get("chargeOffReasonId"); - final String codeValue = (String) chargeOffReasonMap.get("codeValue"); - final String codeDescription = (String) chargeOffReasonMap.get("codeDescription"); - final Integer orderPosition = (Integer) chargeOffReasonMap.get("orderPosition"); - final Boolean active = (Boolean) chargeOffReasonMap.get("isActive"); - final Boolean mandatory = (Boolean) chargeOffReasonMap.get("isMandatory"); - final CodeValueData chargeOffReasonsCodeValue = CodeValueData.builder().id(Long.valueOf(chargeOffReasonId)).name(codeValue) - .description(codeDescription).position(orderPosition).active(active).mandatory(mandatory).build(); + final Long chargeOffReasonId = mapping.getChargeOffReason().getId(); + final String codeValue = mapping.getChargeOffReason().getLabel(); + final String codeDescription = mapping.getChargeOffReason().getDescription(); + final Integer orderPosition = mapping.getChargeOffReason().getPosition(); + final boolean isActive = mapping.getChargeOffReason().isActive(); + final boolean isMandatory = mapping.getChargeOffReason().isMandatory(); + final CodeValueData chargeOffReasonsCodeValue = CodeValueData.builder().id(chargeOffReasonId).name(codeValue) + .description(codeDescription).position(orderPosition).active(isActive).mandatory(isMandatory).build(); final ChargeOffReasonToGLAccountMapper chargeOffReasonToGLAccountMapper = new ChargeOffReasonToGLAccountMapper() .setChargeOffReasonsCodeValue(chargeOffReasonsCodeValue).setChargeOffExpenseAccount(chargeOffExpenseAccount); @@ -394,21 +305,15 @@ public Map fetchAccountMappingDetailsForShareProduct(Long produc final Map accountMappingDetails = new LinkedHashMap<>(8); - final ProductToGLAccountMappingMapper rm = new ProductToGLAccountMappingMapper(); - final String sql = "select " + rm.schema() + " and product_id = ? and payment_type is null and mapping.charge_id is null "; - - final List> listOfProductToGLAccountMaps = this.jdbcTemplate.query(sql, rm, // NOSONAR - new Object[] { PortfolioProductType.SHARES.getValue(), productId }); + final List mappings = productToGLAccountMappingRepository + .findAllByProductIdAndProductTypeAndPaymentTypeIsNullAndChargeIsNull(productId, PortfolioProductType.SHARES.getValue()); if (AccountingRuleType.CASH_BASED.getValue().equals(accountingType)) { - for (final Map productToGLAccountMap : listOfProductToGLAccountMaps) { - final Integer financialAccountType = (Integer) productToGLAccountMap.get("financialAccountType"); - final CashAccountsForShares glAccountForShares = CashAccountsForShares.fromInt(financialAccountType); + for (final ProductToGLAccountMapping mapping : mappings) { + final CashAccountsForShares glAccountForShares = CashAccountsForShares.fromInt(mapping.getFinancialAccountType()); - final Long glAccountId = (Long) productToGLAccountMap.get("glAccountId"); - final String glAccountName = (String) productToGLAccountMap.get("glAccountName"); - final String glCode = (String) productToGLAccountMap.get("glCode"); - final GLAccountData gLAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); + final GLAccountData gLAccountData = new GLAccountData().setId(mapping.getGlAccount().getId()) + .setName(mapping.getGlAccount().getName()).setGlCode(mapping.getGlAccount().getGlCode()); if (glAccountForShares.equals(CashAccountsForShares.SHARES_REFERENCE)) { accountMappingDetails.put(SharesProductAccountingParams.SHARES_REFERENCE.getValue(), gLAccountData); @@ -440,20 +345,16 @@ public List fetchChargeOffReasonMappingsForLoa return fetchChargeOffReasonMappings(PortfolioProductType.LOAN, loanProductId); } - private Map setAccrualPeriodicSavingsProductToGLAccountMaps( - final List> listOfProductToGLAccountMaps) { + private Map setAccrualPeriodicSavingsProductToGLAccountMaps(final List mappings) { final Map accountMappingDetails = new LinkedHashMap<>(8); - for (final Map productToGLAccountMap : listOfProductToGLAccountMaps) { + for (final ProductToGLAccountMapping mapping : mappings) { - final Integer financialAccountType = (Integer) productToGLAccountMap.get("financialAccountType"); - AccrualAccountsForSavings glAccountForSavings = AccrualAccountsForSavings.fromInt(financialAccountType); + AccrualAccountsForSavings glAccountForSavings = AccrualAccountsForSavings.fromInt(mapping.getFinancialAccountType()); if (glAccountForSavings != null) { - final Long glAccountId = (Long) productToGLAccountMap.get("glAccountId"); - final String glAccountName = (String) productToGLAccountMap.get("glAccountName"); - final String glCode = (String) productToGLAccountMap.get("glCode"); - final GLAccountData glAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); + final GLAccountData glAccountData = new GLAccountData().setId(mapping.getGlAccount().getId()) + .setName(mapping.getGlAccount().getName()).setGlCode(mapping.getGlAccount().getGlCode()); // Assets if (glAccountForSavings.equals(AccrualAccountsForSavings.SAVINGS_REFERENCE)) { @@ -487,26 +388,23 @@ private Map setAccrualPeriodicSavingsProductToGLAccountMaps( accountMappingDetails.put(SavingProductAccountingDataParams.LOSSES_WRITTEN_OFF.getValue(), glAccountData); } } else { - log.error("Accounting mapping null {}", financialAccountType); + log.error("Accounting mapping null {}", mapping.getFinancialAccountType()); } } return accountMappingDetails; } - private Map setCashSavingsProductToGLAccountMaps(final List> listOfProductToGLAccountMaps) { + private Map setCashSavingsProductToGLAccountMaps(final List mappings) { final Map accountMappingDetails = new LinkedHashMap<>(8); - for (final Map productToGLAccountMap : listOfProductToGLAccountMaps) { + for (final ProductToGLAccountMapping mapping : mappings) { - final Integer financialAccountType = (Integer) productToGLAccountMap.get("financialAccountType"); - CashAccountsForSavings glAccountForSavings = CashAccountsForSavings.fromInt(financialAccountType); + CashAccountsForSavings glAccountForSavings = CashAccountsForSavings.fromInt(mapping.getFinancialAccountType()); if (glAccountForSavings != null) { - final Long glAccountId = (Long) productToGLAccountMap.get("glAccountId"); - final String glAccountName = (String) productToGLAccountMap.get("glAccountName"); - final String glCode = (String) productToGLAccountMap.get("glCode"); - final GLAccountData glAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); + final GLAccountData glAccountData = new GLAccountData().setId(mapping.getGlAccount().getId()) + .setName(mapping.getGlAccount().getName()).setGlCode(mapping.getGlAccount().getGlCode()); // Assets if (glAccountForSavings.equals(CashAccountsForSavings.SAVINGS_REFERENCE)) { @@ -534,7 +432,7 @@ private Map setCashSavingsProductToGLAccountMaps(final List paymentChannelToFundSourceMappings; public List feeToIncomeAccountMappings; - public List chargeOffReasonsToExpenseMappings; + public List chargeOffReasonsToExpenseMappings; public List penaltyToIncomeAccountMappings; // Multi Disburse @@ -325,6 +325,16 @@ private RateData() {} public List supportedInterestRefundTypes; @Schema(example = "REGULAR") public String chargeOffBehaviour; + + static final class PostChargeOffReasonsToExpenseMappings { + + private PostChargeOffReasonsToExpenseMappings() {} + + @Schema(example = "1") + public Long chargeOffReasonCodeValueId; + @Schema(example = "1") + public Long expenseGLAccountId; + } } @Schema(description = "PostLoanProductsResponse") @@ -1223,10 +1233,37 @@ static final class GetChargeOffReasonsToExpenseMappings { private GetChargeOffReasonsToExpenseMappings() {} - @Schema(example = "1") - public Long chargeOffReasonCodeValueId; - @Schema(example = "12") - public Long expenseGLAccountId; + public GetCodeValueData chargeOffReasonCodeValue; + public GetGLAccountData chargeOffExpenseAccount; + + static final class GetCodeValueData { + + private GetCodeValueData() {} + + @Schema(example = "1") + public Long id; + @Schema(example = "ChargeOffReasons") + public String name; + @Schema(example = "1") + public Integer position; + public String description; + @Schema(example = "true") + public Boolean active; + @Schema(example = "false") + public Boolean mandatory; + } + + static final class GetGLAccountData { + + private GetGLAccountData() {} + + @Schema(example = "1") + public Long id; + @Schema(example = "Written off") + public String name; + @Schema(example = "e4") + public String glCode; + } } static final class GetLoanFeeToIncomeAccountMappings { @@ -1339,7 +1376,7 @@ private GetLoanCharge() {} public GetLoanAccountingMappings accountingMappings; public Set paymentChannelToFundSourceMappings; public Set feeToIncomeAccountMappings; - public Set chargeOffReasonsToExpenseMappings; + public Set chargeOffReasonToGLAccountMappings; @Schema(example = "false") public Boolean isRatesEnabled; @Schema(example = "true") @@ -1599,7 +1636,7 @@ private PutLoanProductsProductIdRequest() {} public Long incomeFromChargeOffPenaltyAccountId; public List paymentChannelToFundSourceMappings; public List feeToIncomeAccountMappings; - public List chargeOffReasonsToExpenseMappings; + public List chargeOffReasonsToExpenseMappings; public List penaltyToIncomeAccountMappings; @Schema(example = "false") public Boolean enableAccrualActivityPosting; diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index b540449e65a..da814c8ad19 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -178,4 +178,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0160_add_acc_product_mapping_product_id_index.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0160_add_acc_product_mapping_product_id_index.xml new file mode 100644 index 00000000000..86675c406b1 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0160_add_acc_product_mapping_product_id_index.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductChargeOffReasonMappingsTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductChargeOffReasonMappingsTest.java index 95d8a735cbc..c87d674ad11 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductChargeOffReasonMappingsTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductChargeOffReasonMappingsTest.java @@ -31,24 +31,22 @@ import org.apache.fineract.client.models.AllowAttributeOverrides; import org.apache.fineract.client.models.ChargeData; import org.apache.fineract.client.models.ChargeToGLAccountMapper; -import org.apache.fineract.client.models.GetChargeOffReasonsToExpenseMappings; import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings; import org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; +import org.apache.fineract.client.models.PostChargeOffReasonsToExpenseMappings; import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PutLoanProductsProductIdRequest; import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.integrationtests.common.BusinessStepHelper; import org.apache.fineract.integrationtests.common.Utils; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; import org.apache.fineract.integrationtests.common.system.CodeHelper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -@ExtendWith(LoanTestLifecycleExtension.class) public class LoanProductChargeOffReasonMappingsTest extends BaseLoanIntegrationTest { private static final String CODE_VALUE_NAME = "ChargeOffReasons"; @@ -77,10 +75,14 @@ public void testCreateLoanProductWithValidChargeOffReason() { final String creationBusinessDay = "15 January 2023"; runAt(creationBusinessDay, () -> { Integer chargeOffReasons = createChargeOffReason(); - Long localLoanProductId = loanTransactionHelper.createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), 15L)) + Long localLoanProductId = loanTransactionHelper.createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), 16L)) .getResourceId(); Assertions.assertNotNull(localLoanProductId); + + final GetLoanProductsProductIdResponse loanProduct = loanTransactionHelper.getLoanProduct(localLoanProductId.intValue()); + Assertions.assertNotNull(loanProduct.getChargeOffReasonToGLAccountMappings()); + Assertions.assertFalse(loanProduct.getChargeOffReasonToGLAccountMappings().isEmpty()); }); } @@ -89,8 +91,8 @@ public void testUpdateLoanProductWithValidChargeOffReason() { final String creationBusinessDay = "15 January 2023"; runAt(creationBusinessDay, () -> { Integer chargeOffReasons = createChargeOffReason(); - List chargeOffReasonsToExpenseMappings = new ArrayList<>(); - GetChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new GetChargeOffReasonsToExpenseMappings(); + List chargeOffReasonsToExpenseMappings = new ArrayList<>(); + PostChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new PostChargeOffReasonsToExpenseMappings(); getChargeOffReasonsToExpenseMappings.setChargeOffReasonCodeValueId(Long.valueOf(chargeOffReasons)); getChargeOffReasonsToExpenseMappings.setExpenseGLAccountId(15L); chargeOffReasonsToExpenseMappings.add(getChargeOffReasonsToExpenseMappings); @@ -139,8 +141,8 @@ private PostLoanProductsRequest loanProductsRequest(Long chargeOffReasonId, Long List penaltyToIncomeAccountMappings = new ArrayList<>(); List feeToIncomeAccountMappings = new ArrayList<>(); - List chargeOffReasonsToExpenseMappings = new ArrayList<>(); - GetChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new GetChargeOffReasonsToExpenseMappings(); + List chargeOffReasonsToExpenseMappings = new ArrayList<>(); + PostChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new PostChargeOffReasonsToExpenseMappings(); getChargeOffReasonsToExpenseMappings.setChargeOffReasonCodeValueId(chargeOffReasonId); getChargeOffReasonsToExpenseMappings.setExpenseGLAccountId(glAccountId); chargeOffReasonsToExpenseMappings.add(getChargeOffReasonsToExpenseMappings); From 1a8755c42f8a757b7fb6eff0e5fd7764bdc37321 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Tue, 17 Dec 2024 18:22:28 +0100 Subject: [PATCH 2/2] FINERACT-1806: Rework Product to GL account mapping and fix charge-off reason typos --- .../domain/ProductToGLAccountMapping.java | 4 +- .../ProductToGLAccountMappingRepository.java | 19 +-- .../ProductToGLAccountMappingHelper.java | 14 +-- ...AccountMappingReadPlatformServiceImpl.java | 45 +++---- .../common/AccountingConstants.java | 2 +- .../ChargeOffReasonToGLAccountMapper.java | 2 +- .../api/LoanProductsApiResourceSwagger.java | 14 +-- .../loanproduct/data/LoanProductData.java | 8 +- .../service/AccountingProcessorHelper.java | 2 +- ...ccrualBasedAccountingProcessorForLoan.java | 4 +- .../CashBasedAccountingProcessorForLoan.java | 119 ++++++++---------- .../api/LoanProductsApiResource.java | 2 +- .../LoanProductDataValidator.java | 4 +- ...oanProductChargeOffReasonMappingsTest.java | 105 +++++++--------- .../common/loans/LoanProductTestBuilder.java | 14 +-- 15 files changed, 161 insertions(+), 197 deletions(-) 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 5e953ff27be..1f602e90d5b 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 @@ -69,9 +69,9 @@ public class ProductToGLAccountMapping extends AbstractPersistableCustom { private CodeValue chargeOffReason; public static ProductToGLAccountMapping createNew(final GLAccount glAccount, final Long productId, final int productType, - final int financialAccountType, final CodeValue chargeOffReasonId) { + final int financialAccountType, final CodeValue chargeOffReason) { return new ProductToGLAccountMapping().setGlAccount(glAccount).setProductId(productId).setProductType(productType) - .setFinancialAccountType(financialAccountType).setChargeOffReason(chargeOffReasonId); + .setFinancialAccountType(financialAccountType).setChargeOffReason(chargeOffReason); } } 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 0dd2b6fb840..9a5436e6c14 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 @@ -63,19 +63,22 @@ List findAllPenaltyToIncomeAccountMappings(@Param("pr List findByProductIdAndProductType(Long productId, int productType); @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.chargeOffReason is not NULL") - List findAllChargesOffReasonsMappings(@Param("productId") Long productId, + List findAllChargeOffReasonsMappings(@Param("productId") Long productId, @Param("productType") int productType); @Query("select mapping from ProductToGLAccountMapping mapping where mapping.chargeOffReason.id =:chargeOffReasonId") - ProductToGLAccountMapping findChargesOffReasonMappingById(@Param("chargeOffReasonId") Integer chargeOffReasonId); + ProductToGLAccountMapping findChargeOffReasonMappingById(@Param("chargeOffReasonId") Integer chargeOffReasonId); - List findAllByProductIdAndProductTypeAndPaymentTypeIsNullAndChargeIsNull(Long productId, - Integer productType); + @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") + List findAllRegularMappings(@Param("productId") Long productId, @Param("productType") Integer productType); - List findAllByProductIdAndProductTypeAndChargeOffReasonIdIsNotNull(Long productId, 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); - List findAllByProductIdAndProductTypeAndPaymentTypeIsNotNull(Long productId, 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); - List findAllByProductIdAndProductTypeAndCharge_PenaltyAndCharge_IdIsNotNull(Long productId, - Integer productType, boolean isPenalty); + @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); } 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 34b9e06d762..52d0a201b99 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 @@ -204,7 +204,7 @@ public void saveChargesToGLAccountMappings(final JsonCommand command, final Json public void saveChargeOffReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, final Map changes, final PortfolioProductType portfolioProductType) { - final String arrayName = LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(); + final String arrayName = LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(); final JsonArray chargeOffReasonToExpenseAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayName, element); if (chargeOffReasonToExpenseAccountMappingArray != null) { @@ -388,17 +388,17 @@ public void updateChargeOffReasonToGLAccountMappings(final JsonCommand command, final Map changes, final PortfolioProductType portfolioProductType) { final List existingChargeOffReasonToGLAccountMappings = this.accountMappingRepository - .findAllChargesOffReasonsMappings(productId, portfolioProductType.getValue()); + .findAllChargeOffReasonsMappings(productId, portfolioProductType.getValue()); final JsonArray chargeOffReasonToGLAccountMappingArray = this.fromApiJsonHelper - .extractJsonArrayNamed(LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(), element); + .extractJsonArrayNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(), element); final Map inputChargeOffReasonToGLAccountMap = new HashMap<>(); final Set existingChargeOffReasons = new HashSet<>(); if (chargeOffReasonToGLAccountMappingArray != null) { if (changes != null) { - changes.put(LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(), - command.jsonFragment(LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue())); + changes.put(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(), + command.jsonFragment(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue())); } for (int i = 0; i < chargeOffReasonToGLAccountMappingArray.size(); i++) { @@ -495,7 +495,7 @@ private void saveChargeOffReasonToExpenseMapping(final Long productId, final Lon final Optional glAccount = accountRepository.findById(expenseAccountId); final boolean reasonMappingExists = this.accountMappingRepository - .findAllChargesOffReasonsMappings(productId, portfolioProductType.getValue()).stream() + .findAllChargeOffReasonsMappings(productId, portfolioProductType.getValue()).stream() .anyMatch(mapping -> mapping.getChargeOffReason().getId().equals(reasonId)); final Optional codeValueOptional = codeValueRepository.findById(reasonId); @@ -577,7 +577,7 @@ public void validateChargeOffMappingsInDatabase(final List mappings) if (codeValue == null) { validationErrors.add(ApiParameterError.parameterError("validation.msg.chargeoffreason.invalid", "Charge-off reason with ID " + chargeOffReasonCodeValueId + " does not exist", - LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue())); + LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue())); } // Validation: expenseGLAccountId must exist as a valid Expense GL account 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 fd3cbfd8543..9b19c65ab88 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 @@ -61,8 +61,8 @@ public Map fetchAccountMappingDetailsForLoanProduct(final Long l final Map accountMappingDetails = new LinkedHashMap<>(8); - final List mappings = productToGLAccountMappingRepository - .findAllByProductIdAndProductTypeAndPaymentTypeIsNullAndChargeIsNull(loanProductId, PortfolioProductType.LOAN.getValue()); + final List mappings = productToGLAccountMappingRepository.findAllRegularMappings(loanProductId, + PortfolioProductType.LOAN.getValue()); if (AccountingValidations.isCashBasedAccounting(accountingType)) { @@ -178,9 +178,8 @@ public Map fetchAccountMappingDetailsForLoanProduct(final Long l @Override public Map fetchAccountMappingDetailsForSavingsProduct(final Long savingsProductId, final Integer accountingType) { - final List mappings = productToGLAccountMappingRepository - .findAllByProductIdAndProductTypeAndPaymentTypeIsNullAndChargeIsNull(savingsProductId, - PortfolioProductType.SAVING.getValue()); + final List mappings = productToGLAccountMappingRepository.findAllRegularMappings(savingsProductId, + PortfolioProductType.SAVING.getValue()); Map accountMappingDetails = null; if (AccountingValidations.isCashBasedAccounting(accountingType)) { @@ -210,14 +209,11 @@ public List fetchPaymentTypeToFundSourceMappingsFo */ private List fetchPaymentTypeToFundSourceMappings(final PortfolioProductType portfolioProductType, final Long loanProductId) { - final List mappings = productToGLAccountMappingRepository - .findAllByProductIdAndProductTypeAndPaymentTypeIsNotNull(loanProductId, portfolioProductType.getValue()); + final List mappings = productToGLAccountMappingRepository.findAllPaymentTypeMappings(loanProductId, + portfolioProductType.getValue()); - List paymentTypeToGLAccountMappers = null; + List paymentTypeToGLAccountMappers = mappings.isEmpty() ? null : new ArrayList<>(); for (final ProductToGLAccountMapping mapping : mappings) { - if (paymentTypeToGLAccountMappers == null) { - paymentTypeToGLAccountMappers = new ArrayList<>(); - } final PaymentTypeData paymentTypeData = PaymentTypeData.instance(mapping.getPaymentType().getId(), mapping.getPaymentType().getName()); final GLAccountData gLAccountData = new GLAccountData().setId(mapping.getGlAccount().getId()) @@ -252,14 +248,12 @@ public List fetchPenaltyToIncomeAccountMappingsForSavin private List fetchChargeToIncomeAccountMappings(final PortfolioProductType portfolioProductType, final Long loanProductId, final boolean penalty) { - final List mappings = productToGLAccountMappingRepository - .findAllByProductIdAndProductTypeAndCharge_PenaltyAndCharge_IdIsNotNull(loanProductId, portfolioProductType.getValue(), - penalty); - List chargeToGLAccountMappers = null; + 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) { - if (chargeToGLAccountMappers == null) { - chargeToGLAccountMappers = new ArrayList<>(); - } 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()) @@ -273,13 +267,10 @@ private List fetchChargeToIncomeAccountMappings(final P private List fetchChargeOffReasonMappings(final PortfolioProductType portfolioProductType, final Long loanProductId) { - final List mappings = productToGLAccountMappingRepository - .findAllByProductIdAndProductTypeAndChargeOffReasonIdIsNotNull(loanProductId, portfolioProductType.getValue()); - List chargeOffReasonToGLAccountMappers = null; + final List mappings = productToGLAccountMappingRepository.findAllChargeOffReasonsMappings(loanProductId, + portfolioProductType.getValue()); + List chargeOffReasonToGLAccountMappers = mappings.isEmpty() ? null : new ArrayList<>(); for (final ProductToGLAccountMapping mapping : mappings) { - if (chargeOffReasonToGLAccountMappers == null) { - chargeOffReasonToGLAccountMappers = new ArrayList<>(); - } final Long glAccountId = mapping.getGlAccount().getId(); final String glAccountName = mapping.getGlAccount().getName(); final String glCode = mapping.getGlAccount().getGlCode(); @@ -294,7 +285,7 @@ private List fetchChargeOffReasonMappings(fina .description(codeDescription).position(orderPosition).active(isActive).mandatory(isMandatory).build(); final ChargeOffReasonToGLAccountMapper chargeOffReasonToGLAccountMapper = new ChargeOffReasonToGLAccountMapper() - .setChargeOffReasonsCodeValue(chargeOffReasonsCodeValue).setChargeOffExpenseAccount(chargeOffExpenseAccount); + .setChargeOffReasonCodeValue(chargeOffReasonsCodeValue).setChargeOffExpenseAccount(chargeOffExpenseAccount); chargeOffReasonToGLAccountMappers.add(chargeOffReasonToGLAccountMapper); } return chargeOffReasonToGLAccountMappers; @@ -305,8 +296,8 @@ public Map fetchAccountMappingDetailsForShareProduct(Long produc final Map accountMappingDetails = new LinkedHashMap<>(8); - final List mappings = productToGLAccountMappingRepository - .findAllByProductIdAndProductTypeAndPaymentTypeIsNullAndChargeIsNull(productId, PortfolioProductType.SHARES.getValue()); + final List mappings = productToGLAccountMappingRepository.findAllRegularMappings(productId, + PortfolioProductType.SHARES.getValue()); if (AccountingRuleType.CASH_BASED.getValue().equals(accountingType)) { for (final ProductToGLAccountMapping mapping : mappings) { diff --git a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java index ce7a2ff9fb4..7976659c392 100644 --- a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java @@ -173,7 +173,7 @@ public enum LoanProductAccountingParams { INCOME_FROM_GOODWILL_CREDIT_INTEREST("incomeFromGoodwillCreditInterestAccountId"), // INCOME_FROM_GOODWILL_CREDIT_FEES("incomeFromGoodwillCreditFeesAccountId"), // INCOME_FROM_GOODWILL_CREDIT_PENALTY("incomeFromGoodwillCreditPenaltyAccountId"), // - CHARGE_OFF_REASONS_TO_EXPENSE("chargeOffReasonsToExpenseMappings"), // + CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS("chargeOffReasonToExpenseAccountMappings"), // EXPENSE_GL_ACCOUNT_ID("expenseGLAccountId"), // CHARGE_OFF_REASON_CODE_VALUE_ID("chargeOffReasonCodeValueId"); // diff --git a/fineract-core/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/data/ChargeOffReasonToGLAccountMapper.java b/fineract-core/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/data/ChargeOffReasonToGLAccountMapper.java index bf881a3fef1..f95e933f2b1 100644 --- a/fineract-core/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/data/ChargeOffReasonToGLAccountMapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/data/ChargeOffReasonToGLAccountMapper.java @@ -31,6 +31,6 @@ public class ChargeOffReasonToGLAccountMapper implements Serializable { private static final long serialVersionUID = 1L; - private CodeValueData chargeOffReasonsCodeValue; + private CodeValueData chargeOffReasonCodeValue; private GLAccountData chargeOffExpenseAccount; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java index 0558a7a5637..76e7c177a95 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java @@ -248,7 +248,7 @@ private PostLoanProductsRequest() {} public Long incomeFromGoodwillCreditPenaltyAccountId; public List paymentChannelToFundSourceMappings; public List feeToIncomeAccountMappings; - public List chargeOffReasonsToExpenseMappings; + public List chargeOffReasonToExpenseAccountMappings; public List penaltyToIncomeAccountMappings; // Multi Disburse @@ -326,9 +326,9 @@ private RateData() {} @Schema(example = "REGULAR") public String chargeOffBehaviour; - static final class PostChargeOffReasonsToExpenseMappings { + static final class PostChargeOffReasonToExpenseAccountMappings { - private PostChargeOffReasonsToExpenseMappings() {} + private PostChargeOffReasonToExpenseAccountMappings() {} @Schema(example = "1") public Long chargeOffReasonCodeValueId; @@ -1229,9 +1229,9 @@ private GetLoanPaymentChannelToFundSourceMappings() {} public Long fundSourceAccountId; } - static final class GetChargeOffReasonsToExpenseMappings { + static final class GetChargeOffReasonToExpenseAccountMappings { - private GetChargeOffReasonsToExpenseMappings() {} + private GetChargeOffReasonToExpenseAccountMappings() {} public GetCodeValueData chargeOffReasonCodeValue; public GetGLAccountData chargeOffExpenseAccount; @@ -1376,7 +1376,7 @@ private GetLoanCharge() {} public GetLoanAccountingMappings accountingMappings; public Set paymentChannelToFundSourceMappings; public Set feeToIncomeAccountMappings; - public Set chargeOffReasonToGLAccountMappings; + public List chargeOffReasonToExpenseAccountMappings; @Schema(example = "false") public Boolean isRatesEnabled; @Schema(example = "true") @@ -1636,7 +1636,7 @@ private PutLoanProductsProductIdRequest() {} public Long incomeFromChargeOffPenaltyAccountId; public List paymentChannelToFundSourceMappings; public List feeToIncomeAccountMappings; - public List chargeOffReasonsToExpenseMappings; + public List chargeOffReasonToExpenseAccountMappings; public List penaltyToIncomeAccountMappings; @Schema(example = "false") public Boolean enableAccrualActivityPosting; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java index 3a1d14beda3..31191601e55 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java @@ -154,7 +154,7 @@ public class LoanProductData implements Serializable { private Collection paymentChannelToFundSourceMappings; private Collection feeToIncomeAccountMappings; private Collection penaltyToIncomeAccountMappings; - private List chargeOffReasonToGLAccountMappings; + private List chargeOffReasonToExpenseAccountMappings; private final boolean enableAccrualActivityPosting; // rates @@ -747,7 +747,7 @@ public static LoanProductData withAccountingDetails(final LoanProductData produc productData.paymentChannelToFundSourceMappings = paymentChannelToFundSourceMappings; productData.feeToIncomeAccountMappings = feeToGLAccountMappings; productData.penaltyToIncomeAccountMappings = penaltyToGLAccountMappings; - productData.chargeOffReasonToGLAccountMappings = chargeOffReasonToGLAccountMappings; + productData.chargeOffReasonToExpenseAccountMappings = chargeOffReasonToGLAccountMappings; return productData; } @@ -865,7 +865,7 @@ public LoanProductData(final Long id, final String name, final String shortName, this.paymentChannelToFundSourceMappings = null; this.feeToIncomeAccountMappings = null; this.penaltyToIncomeAccountMappings = null; - this.chargeOffReasonToGLAccountMappings = null; + this.chargeOffReasonToExpenseAccountMappings = null; this.valueConditionTypeOptions = null; this.principalVariationsForBorrowerCycle = principalVariations; this.interestRateVariationsForBorrowerCycle = interestRateVariations; @@ -1008,7 +1008,7 @@ public LoanProductData(final LoanProductData productData, final Collection accountingBridge } public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Integer chargeOffReasonCodeValue) { - return accountMappingRepository.findChargesOffReasonMappingById(chargeOffReasonCodeValue); + return accountMappingRepository.findChargeOffReasonMappingById(chargeOffReasonCodeValue); } public SavingsDTO populateSavingsDtoFromMap(final Map accountingBridgeData, final boolean cashBasedAccountingEnabled, diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java index eaf22e98d17..773fcadf12a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java @@ -232,7 +232,9 @@ private void createJournalEntriesForChargeOff(LoanDTO loanDTO, LoanTransactionDT // need to fetch if there are account mappings (always one) Integer chargeOffReasonCodeValue = loanDTO.getChargeOffReasonCodeValue(); - ProductToGLAccountMapping mapping = helper.getChargeOffMappingByCodeValue(chargeOffReasonCodeValue); + ProductToGLAccountMapping mapping = chargeOffReasonCodeValue != null + ? helper.getChargeOffMappingByCodeValue(chargeOffReasonCodeValue) + : null; if (mapping != null) { GLAccount accountCredit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java index 4a2f9009c02..770194ef7a5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java @@ -30,6 +30,7 @@ import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; +import org.apache.fineract.accounting.journalentry.data.GLAccountBalanceHolder; import org.apache.fineract.accounting.journalentry.data.LoanDTO; import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO; import org.apache.fineract.infrastructure.core.service.MathUtil; @@ -139,71 +140,59 @@ private void createJournalEntriesForChargeOff(LoanDTO loanDTO, LoanTransactionDT final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); final boolean isReversal = loanTransactionDTO.isReversed(); - Map accountMapForCredit = new LinkedHashMap<>(); - - Map accountMapForDebit = new LinkedHashMap<>(); + GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder(); // principal payment if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) { if (isMarkedFraud) { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), - CashAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), glAccountBalanceHolder); } else { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), - CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), glAccountBalanceHolder); } } // interest payment if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) { populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INTEREST_ON_LOANS.getValue(), - CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), glAccountBalanceHolder); } // handle fees payment if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) { populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_FEES.getValue(), - CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), glAccountBalanceHolder); } // handle penalties payment if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) { populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue(), - CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), glAccountBalanceHolder); } // create credit entries - for (Map.Entry creditEntry : accountMapForCredit.entrySet()) { + for (Map.Entry creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) { + GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey()); this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, - creditEntry.getValue(), isReversal, creditEntry.getKey()); + creditEntry.getValue(), isReversal, glAccount); } - // create debit entries - for (Map.Entry debitEntry : accountMapForDebit.entrySet()) { - this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId, - paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue(), isReversal); + for (Map.Entry debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) { + GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey()); + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, + debitEntry.getValue(), isReversal, glAccount); } - } private void populateCreditDebitMaps(Long loanProductId, BigDecimal transactionPartAmount, Long paymentTypeId, - Integer creditAccountType, Integer debitAccountType, Map accountMapForCredit, - Map accountMapForDebit) { + Integer creditAccountType, Integer debitAccountType, GLAccountBalanceHolder glAccountBalanceHolder) { + // Resolve Credit GLAccount accountCredit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, creditAccountType, paymentTypeId); - if (accountMapForCredit.containsKey(accountCredit)) { - BigDecimal amount = accountMapForCredit.get(accountCredit).add(transactionPartAmount); - accountMapForCredit.put(accountCredit, amount); - } else { - accountMapForCredit.put(accountCredit, transactionPartAmount); - } - Integer accountDebit = returnExistingDebitAccountInMapMatchingGLAccount(loanProductId, paymentTypeId, debitAccountType, - accountMapForDebit); - if (accountMapForDebit.containsKey(accountDebit)) { - BigDecimal amount = accountMapForDebit.get(accountDebit).add(transactionPartAmount); - accountMapForDebit.put(accountDebit, amount); - } else { - accountMapForDebit.put(accountDebit, transactionPartAmount); - } + glAccountBalanceHolder.addToCredit(accountCredit, transactionPartAmount); + // Resolve Debit + GLAccount accountDebit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, debitAccountType, paymentTypeId); + glAccountBalanceHolder.addToDebit(accountDebit, transactionPartAmount); } private Integer returnExistingDebitAccountInMapMatchingGLAccount(Long loanProductId, Long paymentTypeId, Integer accountType, @@ -568,9 +557,7 @@ private void createJournalEntriesForChargeOffLoanRepayments(LoanDTO loanDTO, Loa final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); final boolean isReversal = loanTransactionDTO.isReversed(); - Map accountMapForCredit = new LinkedHashMap<>(); - Map accountMapForDebit = new LinkedHashMap<>(); - + GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder(); BigDecimal totalDebitAmount = new BigDecimal(0); // principal payment @@ -580,35 +567,35 @@ private void createJournalEntriesForChargeOffLoanRepayments(LoanDTO loanDTO, Loa if (isMarkedFraud) { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), - accountMapForCredit, accountMapForDebit); + glAccountBalanceHolder); } else { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), - accountMapForCredit, accountMapForDebit); + glAccountBalanceHolder); } } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { if (isMarkedFraud) { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), - accountMapForCredit, accountMapForDebit); + glAccountBalanceHolder); } else { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), - accountMapForCredit, accountMapForDebit); + glAccountBalanceHolder); } } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - CashAccountsForLoan.GOODWILL_CREDIT.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.GOODWILL_CREDIT.getValue(), glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isRepayment()) { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } else { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } } @@ -619,23 +606,23 @@ private void createJournalEntriesForChargeOffLoanRepayments(LoanDTO loanDTO, Loa if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), - accountMapForCredit, accountMapForDebit); + glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), - accountMapForCredit, accountMapForDebit); + glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isRepayment()) { populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } else { populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INTEREST_ON_LOANS.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } } @@ -646,24 +633,24 @@ private void createJournalEntriesForChargeOffLoanRepayments(LoanDTO loanDTO, Loa if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), - accountMapForCredit, accountMapForDebit); + glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), - accountMapForCredit, accountMapForDebit); + glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isRepayment()) { populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } else { populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_FEES.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } } @@ -674,24 +661,24 @@ private void createJournalEntriesForChargeOffLoanRepayments(LoanDTO loanDTO, Loa if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), - accountMapForCredit, accountMapForDebit); + glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), - accountMapForCredit, accountMapForDebit); + glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isRepayment()) { populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } else { populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } } @@ -701,26 +688,27 @@ private void createJournalEntriesForChargeOffLoanRepayments(LoanDTO loanDTO, Loa totalDebitAmount = totalDebitAmount.add(overPaymentAmount); if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, CashAccountsForLoan.OVERPAYMENT.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, CashAccountsForLoan.OVERPAYMENT.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, CashAccountsForLoan.OVERPAYMENT.getValue(), - CashAccountsForLoan.GOODWILL_CREDIT.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.GOODWILL_CREDIT.getValue(), glAccountBalanceHolder); } else { populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, CashAccountsForLoan.OVERPAYMENT.getValue(), - CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } } // create credit entries - for (Map.Entry creditEntry : accountMapForCredit.entrySet()) { + for (Map.Entry creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) { + GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey()); this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, - creditEntry.getValue(), isReversal, creditEntry.getKey()); + creditEntry.getValue(), isReversal, glAccount); } /*** create a single debit entry (or reversal) for the entire amount **/ @@ -732,9 +720,10 @@ private void createJournalEntriesForChargeOffLoanRepayments(LoanDTO loanDTO, Loa loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal); } else { // create debit entries - for (Map.Entry debitEntry : accountMapForDebit.entrySet()) { - this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId, - paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue(), isReversal); + for (Map.Entry debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) { + GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey()); + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, + debitEntry.getValue(), isReversal, glAccount); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index c157731f5ea..b68a400054c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -162,7 +162,7 @@ public class LoanProductsApiResource { @Operation(summary = "Create a Loan Product", description = "Depending of the Accounting Rule (accountingRule) selected, additional fields with details of the appropriate Ledger Account identifiers would need to be passed in.\n" + "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n" + "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n" - + "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, chargeOffReasonsToExpenseMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n" + + "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, chargeOffReasonToExpenseAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n" + "Additional Mandatory Fields for Cash(2) based accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n" + "Additional Mandatory Fields for periodic (3) and upfront (4)accrual accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n" + "Additional Mandatory Fields if interest recalculation is enabled(true): interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType\n\n" diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index f967e897b91..cfc230a3357 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -144,7 +144,7 @@ public final class LoanProductDataValidator { LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), - LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(), + LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(), LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.PRINCIPAL_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME, @@ -1992,7 +1992,7 @@ private void validateChargeToIncomeAccountMappings(final DataValidatorBuilder ba } private void validateChargeOffToExpenseMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) { - String parameterName = LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(); + String parameterName = LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(); if (this.fromApiJsonHelper.parameterExists(parameterName, element)) { final JsonArray chargeOffToExpenseMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductChargeOffReasonMappingsTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductChargeOffReasonMappingsTest.java index c87d674ad11..84d518adc0d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductChargeOffReasonMappingsTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductChargeOffReasonMappingsTest.java @@ -20,11 +20,6 @@ import static org.apache.fineract.integrationtests.common.funds.FundsResourceHandler.createFund; -import io.restassured.builder.RequestSpecBuilder; -import io.restassured.builder.ResponseSpecBuilder; -import io.restassured.http.ContentType; -import io.restassured.specification.RequestSpecification; -import io.restassured.specification.ResponseSpecification; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -34,84 +29,60 @@ import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings; import org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings; import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; -import org.apache.fineract.client.models.PostChargeOffReasonsToExpenseMappings; +import org.apache.fineract.client.models.PostChargeOffReasonToExpenseAccountMappings; import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PutLoanProductsProductIdRequest; import org.apache.fineract.client.util.CallFailedRuntimeException; -import org.apache.fineract.integrationtests.common.BusinessStepHelper; import org.apache.fineract.integrationtests.common.Utils; -import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.accounting.Account; import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; import org.apache.fineract.integrationtests.common.system.CodeHelper; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; public class LoanProductChargeOffReasonMappingsTest extends BaseLoanIntegrationTest { private static final String CODE_VALUE_NAME = "ChargeOffReasons"; - - private static ResponseSpecification responseSpec; - private static RequestSpecification requestSpec; - private static LoanTransactionHelper loanTransactionHelper; - - @BeforeAll - public static void setup() { - Utils.initializeRESTAssured(); - requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); - requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); - requestSpec.header("Fineract-Platform-TenantId", "default"); - responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); - loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); - BusinessStepHelper businessStepHelper = new BusinessStepHelper(); - // setup COB Business Steps to prevent test failing due other integration test configurations - businessStepHelper.updateSteps("LOAN_CLOSE_OF_BUSINESS", "APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", - "CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES", - "EXTERNAL_ASSET_OWNER_TRANSFER", "ACCRUAL_ACTIVITY_POSTING"); - } + private final Account expenseAccount = accountHelper.createExpenseAccount(); + private final Account otherExpenseAccount = accountHelper.createExpenseAccount(); @Test - public void testCreateLoanProductWithValidChargeOffReason() { - final String creationBusinessDay = "15 January 2023"; - runAt(creationBusinessDay, () -> { + public void testCreateAndUpdateLoanProductWithValidChargeOffReason() { + runAt("15 January 2023", () -> { Integer chargeOffReasons = createChargeOffReason(); - Long localLoanProductId = loanTransactionHelper.createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), 16L)) + Long localLoanProductId = loanTransactionHelper + .createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), expenseAccount.getAccountID().longValue())) .getResourceId(); Assertions.assertNotNull(localLoanProductId); - final GetLoanProductsProductIdResponse loanProduct = loanTransactionHelper.getLoanProduct(localLoanProductId.intValue()); - Assertions.assertNotNull(loanProduct.getChargeOffReasonToGLAccountMappings()); - Assertions.assertFalse(loanProduct.getChargeOffReasonToGLAccountMappings().isEmpty()); - }); - } + GetLoanProductsProductIdResponse loanProductDetails = loanTransactionHelper.getLoanProduct(localLoanProductId.intValue()); + Assertions.assertEquals(expenseAccount.getAccountID().longValue(), + loanProductDetails.getChargeOffReasonToExpenseAccountMappings().get(0).getChargeOffExpenseAccount().getId()); + Assertions.assertEquals(Long.valueOf(chargeOffReasons), + loanProductDetails.getChargeOffReasonToExpenseAccountMappings().get(0).getChargeOffReasonCodeValue().getId()); - @Test - public void testUpdateLoanProductWithValidChargeOffReason() { - final String creationBusinessDay = "15 January 2023"; - runAt(creationBusinessDay, () -> { - Integer chargeOffReasons = createChargeOffReason(); - List chargeOffReasonsToExpenseMappings = new ArrayList<>(); - PostChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new PostChargeOffReasonsToExpenseMappings(); - getChargeOffReasonsToExpenseMappings.setChargeOffReasonCodeValueId(Long.valueOf(chargeOffReasons)); - getChargeOffReasonsToExpenseMappings.setExpenseGLAccountId(15L); - chargeOffReasonsToExpenseMappings.add(getChargeOffReasonsToExpenseMappings); + List chargeOffReasonToExpenseAccountMappings = createPostChargeOffReasonToExpenseAccountMappings( + Long.valueOf(chargeOffReasons), otherExpenseAccount.getAccountID().longValue()); - Long localLoanProductId = loanTransactionHelper.updateLoanProduct(1L, - new PutLoanProductsProductIdRequest().locale("en").chargeOffReasonsToExpenseMappings(chargeOffReasonsToExpenseMappings)) - .getResourceId(); + loanTransactionHelper.updateLoanProduct(localLoanProductId, new PutLoanProductsProductIdRequest().locale("en") + .chargeOffReasonToExpenseAccountMappings(chargeOffReasonToExpenseAccountMappings)).getResourceId(); - Assertions.assertNotNull(localLoanProductId); + loanProductDetails = loanTransactionHelper.getLoanProduct(localLoanProductId.intValue()); + Assertions.assertEquals(otherExpenseAccount.getAccountID().longValue(), + loanProductDetails.getChargeOffReasonToExpenseAccountMappings().get(0).getChargeOffExpenseAccount().getId()); + Assertions.assertEquals(Long.valueOf(chargeOffReasons), + loanProductDetails.getChargeOffReasonToExpenseAccountMappings().get(0).getChargeOffReasonCodeValue().getId()); }); } @Test public void testCreateLoanProductWithInvalidGLAccount() { - final String creationBusinessDay = "15 January 2023"; - runAt(creationBusinessDay, () -> { + runAt("15 January 2023", () -> { try { Integer chargeOffReasons = createChargeOffReason(); - loanTransactionHelper.createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), 9999L)); + loanTransactionHelper.createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), -1L)); } catch (CallFailedRuntimeException e) { Assertions.assertTrue(e.getMessage().contains("validation.msg.glaccount.not.found")); } @@ -120,10 +91,9 @@ public void testCreateLoanProductWithInvalidGLAccount() { @Test public void testCreateLoanProductWithInvalidChargeOffReason() { - final String creationBusinessDay = "15 January 2023"; - runAt(creationBusinessDay, () -> { + runAt("15 January 2023", () -> { try { - loanTransactionHelper.createLoanProduct(loanProductsRequest(1L, 12L)); + loanTransactionHelper.createLoanProduct(loanProductsRequest(-1L, expenseAccount.getAccountID().longValue())); } catch (CallFailedRuntimeException e) { Assertions.assertTrue(e.getMessage().contains("validation.msg.chargeoffreason.invalid")); } @@ -141,11 +111,8 @@ private PostLoanProductsRequest loanProductsRequest(Long chargeOffReasonId, Long List penaltyToIncomeAccountMappings = new ArrayList<>(); List feeToIncomeAccountMappings = new ArrayList<>(); - List chargeOffReasonsToExpenseMappings = new ArrayList<>(); - PostChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new PostChargeOffReasonsToExpenseMappings(); - getChargeOffReasonsToExpenseMappings.setChargeOffReasonCodeValueId(chargeOffReasonId); - getChargeOffReasonsToExpenseMappings.setExpenseGLAccountId(glAccountId); - chargeOffReasonsToExpenseMappings.add(getChargeOffReasonsToExpenseMappings); + List chargeOffReasonToExpenseAccountMappings = createPostChargeOffReasonToExpenseAccountMappings( + chargeOffReasonId, glAccountId); List paymentChannelToFundSourceMappings = new ArrayList<>(); GetLoanPaymentChannelToFundSourceMappings loanPaymentChannelToFundSourceMappings = new GetLoanPaymentChannelToFundSourceMappings(); @@ -248,7 +215,8 @@ private PostLoanProductsRequest loanProductsRequest(Long chargeOffReasonId, Long .delinquencyBucketId(delinquencyBucketId.longValue())// .paymentChannelToFundSourceMappings(paymentChannelToFundSourceMappings)// .penaltyToIncomeAccountMappings(penaltyToIncomeAccountMappings)// - .chargeOffReasonsToExpenseMappings(chargeOffReasonsToExpenseMappings).feeToIncomeAccountMappings(feeToIncomeAccountMappings)// + .chargeOffReasonToExpenseAccountMappings(chargeOffReasonToExpenseAccountMappings) + .feeToIncomeAccountMappings(feeToIncomeAccountMappings)// .isInterestRecalculationEnabled(true)// .preClosureInterestCalculationStrategy(1)// .rescheduleStrategyMethod(3)// @@ -258,6 +226,17 @@ private PostLoanProductsRequest loanProductsRequest(Long chargeOffReasonId, Long .allowPartialPeriodInterestCalcualtion(false);// } + @NotNull + private static List createPostChargeOffReasonToExpenseAccountMappings( + Long chargeOffReasonId, Long glAccountId) { + List chargeOffReasonToExpenseAccountMappings = new ArrayList<>(); + PostChargeOffReasonToExpenseAccountMappings chargeOffReasonToExpenseAccountMapping = new PostChargeOffReasonToExpenseAccountMappings(); + chargeOffReasonToExpenseAccountMapping.setChargeOffReasonCodeValueId(chargeOffReasonId); + chargeOffReasonToExpenseAccountMapping.setExpenseGLAccountId(glAccountId); + chargeOffReasonToExpenseAccountMappings.add(chargeOffReasonToExpenseAccountMapping); + return chargeOffReasonToExpenseAccountMappings; + } + private Integer createChargeOffReason() { Integer chargeOffReasonId; HashMap codes = CodeHelper.getCodeByName(requestSpec, responseSpec, CODE_VALUE_NAME); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index eded00f83ca..40116b9aead 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -108,7 +108,7 @@ public class LoanProductTestBuilder { private List> feeToIncomeAccountMappings = null; private List> penaltyToIncomeAccountMappings = null; - private List> chargeOffReasonsToExpenseMappings = null; + private List> chargeOffReasonToExpenseAccountMappings = null; private Account feeAndPenaltyAssetAccount; private Boolean multiDisburseLoan = false; @@ -306,8 +306,8 @@ public HashMap build(final String chargeId, final Integer delinq map.put("penaltyToIncomeAccountMappings", this.penaltyToIncomeAccountMappings); } - if (this.chargeOffReasonsToExpenseMappings != null) { - map.put("chargeOffReasonsToExpenseMappings", this.chargeOffReasonsToExpenseMappings); + if (this.chargeOffReasonToExpenseAccountMappings != null) { + map.put("chargeOffReasonToExpenseAccountMappings", this.chargeOffReasonToExpenseAccountMappings); } if (this.dueDaysForRepaymentEvent != null) { @@ -812,14 +812,14 @@ public LoanProductTestBuilder withChargeOffBehaviour(LoanChargeOffBehaviour char return this; } - public LoanProductTestBuilder withChargeOffReasonsToExpenseMappings(final Long reasonId, final Long accountId) { - if (this.chargeOffReasonsToExpenseMappings == null) { - this.chargeOffReasonsToExpenseMappings = new ArrayList<>(); + public LoanProductTestBuilder withchargeOffReasonToExpenseAccountMappings(final Long reasonId, final Long accountId) { + if (this.chargeOffReasonToExpenseAccountMappings == null) { + this.chargeOffReasonToExpenseAccountMappings = new ArrayList<>(); } Map newMap = new HashMap<>(); newMap.put("chargeOffReasonCodeValueId", reasonId); newMap.put("expenseGLAccountId", accountId); - this.chargeOffReasonsToExpenseMappings.add(newMap); + this.chargeOffReasonToExpenseAccountMappings.add(newMap); return this; }