From cb70555485ad401b6c88d708aa2e60c9c5c6dafe Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Mon, 8 Sep 2025 21:05:22 -0500 Subject: [PATCH] FINERACT-2374: Advance Accounting rule for classification type --- .../domain/ProductToGLAccountMapping.java | 14 +- .../ProductToGLAccountMappingRepository.java | 21 ++- .../ProductToGLAccountMappingHelper.java | 160 ++++++++++++++++++ ...ToGLAccountMappingReadPlatformService.java | 5 + ...AccountMappingReadPlatformServiceImpl.java | 51 ++++-- .../common/AccountingConstants.java | 7 +- .../data/AdvancedMappingtDTO.java | 31 ++++ .../data/ClassificationToGLAccountData.java | 36 ++++ .../api/CodeValuesApiResourceSwagger.java | 6 +- .../codes/api/CodesApiResourceSwagger.java | 0 .../codes/mapper/CodeValueMapper.java | 36 ++++ .../LoanProductToGLAccountMappingHelper.java | 24 +++ .../data/AccountingBridgeDataDTO.java | 3 + ...ortizationAllocationMappingRepository.java | 9 + .../domain/LoanTransactionRepository.java | 8 + .../api/LoanProductsApiResourceSwagger.java | 54 ++++-- .../loanproduct/data/LoanProductData.java | 23 ++- .../CreateLoanProductCommandHandler.java | 9 +- .../accounting/journalentry/data/LoanDTO.java | 2 + .../service/AccountingProcessorHelper.java | 31 +++- ...ccrualBasedAccountingProcessorForLoan.java | 114 ++++++++++++- ...WritePlatformServiceJpaRepositoryImpl.java | 37 +++- .../AccountingJournalEntryConfiguration.java | 8 +- ...ccountMappingWritePlatformServiceImpl.java | 12 ++ .../api/LoanProductsApiResource.java | 20 ++- .../LoanProductDataValidator.java | 74 +++++++- .../db/changelog/tenant/changelog-tenant.xml | 1 + ...assification_id_to_acc_product_mapping.xml | 52 ++++++ ...ateJournalEntriesForChargeOffLoanTest.java | 2 +- .../integrationtests/LoanBuyDownFeeTest.java | 134 +++++++++++++-- .../LoanCapitalizedIncomeTest.java | 105 ++++++++++++ 31 files changed, 1023 insertions(+), 66 deletions(-) create mode 100755 fineract-core/src/main/java/org/apache/fineract/accounting/journalentry/data/AdvancedMappingtDTO.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/data/ClassificationToGLAccountData.java rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/codes/api/CodeValuesApiResourceSwagger.java (95%) rename {fineract-provider => fineract-core}/src/main/java/org/apache/fineract/infrastructure/codes/api/CodesApiResourceSwagger.java (100%) create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/mapper/CodeValueMapper.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0198_add_classification_id_to_acc_product_mapping.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 1f602e90d..12edabfae 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 @@ -68,10 +68,20 @@ public class ProductToGLAccountMapping extends AbstractPersistableCustom { @JoinColumn(name = "charge_off_reason_id", nullable = true) private CodeValue chargeOffReason; + @ManyToOne + @JoinColumn(name = "capitalized_income_classification_id", nullable = true) + private CodeValue capitalizedIncomeClassification; + + @ManyToOne + @JoinColumn(name = "buydown_fee_classification_id", nullable = true) + private CodeValue buydownFeeClassification; + public static ProductToGLAccountMapping createNew(final GLAccount glAccount, final Long productId, final int productType, - final int financialAccountType, final CodeValue chargeOffReason) { + final int financialAccountType, final CodeValue chargeOffReason, final CodeValue capitalizedIncomeClassification, + final CodeValue buydownFeeClassification) { return new ProductToGLAccountMapping().setGlAccount(glAccount).setProductId(productId).setProductType(productType) - .setFinancialAccountType(financialAccountType).setChargeOffReason(chargeOffReason); + .setFinancialAccountType(financialAccountType).setChargeOffReason(chargeOffReason) + .setCapitalizedIncomeClassification(capitalizedIncomeClassification).setBuydownFeeClassification(buydownFeeClassification); } } 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 2b2bc2117..917fbdffd 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 @@ public interface ProductToGLAccountMappingRepository @Param("productType") int productType, @Param("financialAccountType") int financialAccountType, @Param("chargeId") Long ChargeId); - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReason is NULL") + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReason is NULL and mapping.capitalizedIncomeClassification is NULL and mapping.buydownFeeClassification is NULL") ProductToGLAccountMapping findCoreProductToFinAccountMapping(@Param("productId") Long productId, @Param("productType") int productType, @Param("financialAccountType") int financialAccountType); @@ -70,7 +70,7 @@ public interface ProductToGLAccountMappingRepository ProductToGLAccountMapping findChargeOffReasonMapping(@Param("productId") Long productId, @Param("productType") Integer productType, @Param("chargeOffReasonId") Long chargeOffReasonId); - @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL") + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL AND mapping.capitalizedIncomeClassification is NULL AND mapping.buydownFeeClassification is NULL") List findAllRegularMappings(@Param("productId") Long productId, @Param("productType") Integer productType); @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.paymentType is not NULL") @@ -82,4 +82,21 @@ public interface ProductToGLAccountMappingRepository @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge.penalty = FALSE") List findAllFeeMappings(@Param("productId") Long productId, @Param("productType") Integer productType); + + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.capitalizedIncomeClassification is not NULL") + List findAllCapitalizedIncomeClassificationsMappings(@Param("productId") Long productId, + @Param("productType") int productType); + + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.buydownFeeClassification is not NULL") + List findAllBuyDownFeeClassificationsMappings(@Param("productId") Long productId, + @Param("productType") int productType); + + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.buydownFeeClassification.id = :classificationId AND mapping.productId = :productId AND mapping.productType = :productType") + ProductToGLAccountMapping findBuydownFeeClassificationMapping(@Param("productId") Long productId, + @Param("productType") Integer productType, @Param("classificationId") Long classificationId); + + @Query("select mapping from ProductToGLAccountMapping mapping where mapping.capitalizedIncomeClassification.id = :classificationId AND mapping.productId = :productId AND mapping.productType = :productType") + ProductToGLAccountMapping findCapitalizedIncomeClassificationMapping(@Param("productId") Long productId, + @Param("productType") Integer productType, @Param("classificationId") Long classificationId); + } 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 dcd9ec44c..b37580c87 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 @@ -227,6 +227,30 @@ public class ProductToGLAccountMappingHelper { } } + public void saveClassificationToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, + final Map changes, final PortfolioProductType portfolioProductType, + final LoanProductAccountingParams classificationParameter) { + + final String arrayName = classificationParameter.getValue(); + final JsonArray classificationToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayName, element); + + if (classificationToIncomeAccountMappingArray != null) { + if (changes != null) { + changes.put(arrayName, command.jsonFragment(arrayName)); + } + + for (int i = 0; i < classificationToIncomeAccountMappingArray.size(); i++) { + final JsonObject jsonObject = classificationToIncomeAccountMappingArray.get(i).getAsJsonObject(); + final Long classificationId = jsonObject.get(LoanProductAccountingParams.CLASSIFICATION_CODE_VALUE_ID.getValue()) + .getAsLong(); + final Long incomeAccountId = jsonObject.get(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue()).getAsLong(); + + saveClassificationToIncomeMapping(productId, classificationId, incomeAccountId, portfolioProductType, + classificationParameter); + } + } + } + /** * @param command * @param element @@ -448,6 +472,75 @@ public class ProductToGLAccountMappingHelper { } } + public void updateClassificationToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, + final Map changes, final PortfolioProductType portfolioProductType, + final LoanProductAccountingParams classificationParameter) { + + final List existingClassificationToGLAccountMappings = classificationParameter + .equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS) + ? this.accountMappingRepository.findAllCapitalizedIncomeClassificationsMappings(productId, + portfolioProductType.getValue()) + : this.accountMappingRepository.findAllBuyDownFeeClassificationsMappings(productId, + portfolioProductType.getValue()); + + final JsonArray classificationToGLAccountMappingArray = this.fromApiJsonHelper + .extractJsonArrayNamed(classificationParameter.getValue(), element); + + final Map inputClassificationToGLAccountMap = new HashMap<>(); + + final Set existingClassifications = new HashSet<>(); + if (classificationToGLAccountMappingArray != null) { + if (changes != null) { + changes.put(classificationParameter.getValue(), command.jsonFragment(classificationParameter.getValue())); + } + + for (int i = 0; i < classificationToGLAccountMappingArray.size(); i++) { + final JsonObject jsonObject = classificationToGLAccountMappingArray.get(i).getAsJsonObject(); + final Long incomeGlAccountId = jsonObject.get(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue()).getAsLong(); + final Long classificationCodeValueId = jsonObject.get(LoanProductAccountingParams.CLASSIFICATION_CODE_VALUE_ID.getValue()) + .getAsLong(); + inputClassificationToGLAccountMap.put(classificationCodeValueId, incomeGlAccountId); + } + + // If input map is empty, delete all existing mappings + if (inputClassificationToGLAccountMap.isEmpty()) { + this.accountMappingRepository.deleteAllInBatch(existingClassificationToGLAccountMappings); + } else { + for (final ProductToGLAccountMapping existingClassificationToGLAccountMapping : existingClassificationToGLAccountMappings) { + final Long currentClassificationId = classificationParameter + .equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS) + ? existingClassificationToGLAccountMapping.getCapitalizedIncomeClassification().getId() + : existingClassificationToGLAccountMapping.getBuydownFeeClassification().getId(); + + if (currentClassificationId != null) { + existingClassifications.add(currentClassificationId); + // update existing mappings (if required) + if (inputClassificationToGLAccountMap.containsKey(currentClassificationId)) { + final Long newGLAccountId = inputClassificationToGLAccountMap.get(currentClassificationId); + if (!newGLAccountId.equals(existingClassificationToGLAccountMapping.getGlAccount().getId())) { + final Optional glAccount = accountRepository.findById(newGLAccountId); + if (glAccount.isPresent()) { + existingClassificationToGLAccountMapping.setGlAccount(glAccount.get()); + this.accountMappingRepository.saveAndFlush(existingClassificationToGLAccountMapping); + } + } + } // deleted previous record + else { + this.accountMappingRepository.delete(existingClassificationToGLAccountMapping); + } + } + } + + // only the newly added + for (Map.Entry entry : inputClassificationToGLAccountMap.entrySet().stream() + .filter(e -> !existingClassifications.contains(e.getKey())).toList()) { + saveClassificationToIncomeMapping(productId, entry.getKey(), entry.getValue(), portfolioProductType, + classificationParameter); + } + } + } + } + /** * @param productId * @@ -514,6 +607,39 @@ public class ProductToGLAccountMappingHelper { } } + private void saveClassificationToIncomeMapping(final Long productId, final Long classificationId, final Long incomeAccountId, + final PortfolioProductType portfolioProductType, final LoanProductAccountingParams classificationParameter) { + + final Optional glAccount = accountRepository.findById(incomeAccountId); + + boolean classificationMappingExists = false; + if (classificationParameter.equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS)) { + classificationMappingExists = this.accountMappingRepository + .findAllCapitalizedIncomeClassificationsMappings(productId, portfolioProductType.getValue()).stream() + .anyMatch(mapping -> mapping.getCapitalizedIncomeClassification().getId().equals(classificationId)); + } else { + classificationMappingExists = this.accountMappingRepository + .findAllBuyDownFeeClassificationsMappings(productId, portfolioProductType.getValue()).stream() + .anyMatch(mapping -> mapping.getBuydownFeeClassification().getId().equals(classificationId)); + } + + final Optional codeValueOptional = codeValueRepository.findById(classificationId); + + if (glAccount.isPresent() && !classificationMappingExists && codeValueOptional.isPresent()) { + final ProductToGLAccountMapping accountMapping = new ProductToGLAccountMapping().setGlAccount(glAccount.get()) + .setProductId(productId).setProductType(portfolioProductType.getValue()) + .setFinancialAccountType(CashAccountsForLoan.CLASSIFICATION_INCOME.getValue()); + + if (classificationParameter.equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS)) { + accountMapping.setCapitalizedIncomeClassification(codeValueOptional.get()); + } else { + accountMapping.setBuydownFeeClassification(codeValueOptional.get()); + } + + this.accountMappingRepository.saveAndFlush(accountMapping); + } + } + private List getAllowedAccountTypesForFeeMapping() { List allowedAccountTypes = new ArrayList<>(); allowedAccountTypes.add(GLAccountType.INCOME); @@ -610,4 +736,38 @@ public class ProductToGLAccountMappingHelper { throw new PlatformApiDataValidationException(validationErrors); } } + + public void validateClassificationMappingsInDatabase(final List mappings, final String dataCodeName) { + final List validationErrors = new ArrayList<>(); + + for (JsonObject jsonObject : mappings) { + final Long incomeGlAccountId = this.fromApiJsonHelper.extractLongNamed(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), + jsonObject); + final Long classificationCodeValueId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.CLASSIFICATION_CODE_VALUE_ID.getValue(), jsonObject); + + // Validation: classificationCodeValueId must exist in the database + final CodeValue codeValue = this.codeValueRepository.findByCodeNameAndId(dataCodeName, classificationCodeValueId); + if (codeValue == null) { + validationErrors.add(ApiParameterError.parameterError("validation.msg.classification.invalid", + "Classification with ID " + classificationCodeValueId + " does not exist", dataCodeName)); + } + + // Validation: expenseGLAccountId must exist as a valid Expense GL account + final Optional glAccount = accountRepository.findById(incomeGlAccountId); + + if (glAccount.isEmpty() || !GLAccountType.fromInt(glAccount.get().getType()).isIncomeType()) { + validationErrors.add(ApiParameterError.parameterError("validation.msg.glaccount.not.found", + "GL Account with ID " + incomeGlAccountId + " does not exist or is not an Income GL account", + LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue())); + + } + } + + // Throw all collected validation errors, if any + if (!validationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(validationErrors); + } + } + } diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java index 6c89ab1a3..22ccc37c1 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java @@ -20,8 +20,10 @@ package org.apache.fineract.accounting.producttoaccountmapping.service; import java.util.List; import java.util.Map; +import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; +import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; public interface ProductToGLAccountMappingReadPlatformService { @@ -49,4 +51,7 @@ public interface ProductToGLAccountMappingReadPlatformService { List fetchFeeToIncomeAccountMappingsForShareProduct(Long productId); List fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId); + + List fetchClassificationMappingsForLoanProduct(Long loanProductId, + LoanProductAccountingParams classificationParameter); } 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 6e2c44c23..1fcb7bf6d 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 @@ -30,6 +30,7 @@ import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsFor import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares; import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingDataParams; +import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; import org.apache.fineract.accounting.common.AccountingConstants.SavingProductAccountingDataParams; import org.apache.fineract.accounting.common.AccountingConstants.SharesProductAccountingParams; import org.apache.fineract.accounting.common.AccountingRuleType; @@ -37,14 +38,15 @@ import org.apache.fineract.accounting.common.AccountingValidations; import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; +import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData; 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.codes.mapper.CodeValueMapper; import org.apache.fineract.portfolio.PortfolioProductType; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @Slf4j @@ -52,9 +54,8 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class ProductToGLAccountMappingReadPlatformServiceImpl implements ProductToGLAccountMappingReadPlatformService { - private final JdbcTemplate jdbcTemplate; - private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; + private final CodeValueMapper codeValueMapper; @Override public Map fetchAccountMappingDetailsForLoanProduct(final Long loanProductId, final Integer accountingType) { @@ -283,14 +284,7 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product final String glAccountName = mapping.getGlAccount().getName(); final String glCode = mapping.getGlAccount().getGlCode(); final GLAccountData chargeOffExpenseAccount = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); - 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 CodeValueData chargeOffReasonsCodeValue = codeValueMapper.map(mapping.getChargeOffReason()); final ChargeOffReasonToGLAccountMapper chargeOffReasonToGLAccountMapper = new ChargeOffReasonToGLAccountMapper() .setChargeOffReasonCodeValue(chargeOffReasonsCodeValue).setExpenseAccount(chargeOffExpenseAccount); @@ -299,6 +293,35 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product return chargeOffReasonToGLAccountMappers; } + private List fetchClassificationMappings(final PortfolioProductType portfolioProductType, + final Long loanProductId, LoanProductAccountingParams classificationParameter) { + final List mappings = classificationParameter + .equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS) + ? productToGLAccountMappingRepository.findAllCapitalizedIncomeClassificationsMappings(loanProductId, + portfolioProductType.getValue()) + : productToGLAccountMappingRepository.findAllBuyDownFeeClassificationsMappings(loanProductId, + portfolioProductType.getValue()); + + productToGLAccountMappingRepository.findAllChargeOffReasonsMappings(loanProductId, portfolioProductType.getValue()); + List classificationToGLAccountMappers = mappings.isEmpty() ? null : new ArrayList<>(); + for (final ProductToGLAccountMapping mapping : mappings) { + final Long glAccountId = mapping.getGlAccount().getId(); + final String glAccountName = mapping.getGlAccount().getName(); + final String glCode = mapping.getGlAccount().getGlCode(); + final GLAccountData glAccountData = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); + + final CodeValueData classificationCodeValue = classificationParameter + .equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS) + ? codeValueMapper.map(mapping.getCapitalizedIncomeClassification()) + : codeValueMapper.map(mapping.getBuydownFeeClassification()); + + final ClassificationToGLAccountData classificationToGLAccountMapper = new ClassificationToGLAccountData() + .setClassificationCodeValue(classificationCodeValue).setIncomeAccount(glAccountData); + classificationToGLAccountMappers.add(classificationToGLAccountMapper); + } + return classificationToGLAccountMappers; + } + @Override public Map fetchAccountMappingDetailsForShareProduct(Long productId, Integer accountingType) { @@ -344,6 +367,12 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product return fetchChargeOffReasonMappings(PortfolioProductType.LOAN, loanProductId); } + @Override + public List fetchClassificationMappingsForLoanProduct(Long loanProductId, + LoanProductAccountingParams classificationParameter) { + return fetchClassificationMappings(PortfolioProductType.LOAN, loanProductId, classificationParameter); + } + private Map setAccrualPeriodicSavingsProductToGLAccountMaps(final List mappings) { final Map accountMappingDetails = new LinkedHashMap<>(8); 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 1c50780e8..7435a46d9 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 @@ -53,7 +53,9 @@ public final class AccountingConstants { INCOME_FROM_CHARGE_OFF_PENALTY(18), // INCOME_FROM_GOODWILL_CREDIT_INTEREST(19), // INCOME_FROM_GOODWILL_CREDIT_FEES(20), // - INCOME_FROM_GOODWILL_CREDIT_PENALTY(21); // + INCOME_FROM_GOODWILL_CREDIT_PENALTY(21), // + CLASSIFICATION_INCOME(22), // + ; private final Integer value; @@ -185,6 +187,9 @@ public final class AccountingConstants { INCOME_FROM_CAPITALIZATION("incomeFromCapitalizationAccountId"), // BUY_DOWN_EXPENSE("buyDownExpenseAccountId"), // INCOME_FROM_BUY_DOWN("incomeFromBuyDownAccountId"), // + CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS("capitalizedIncomeClassificationToIncomeAccountMappings"), // + BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS("buydownfeeClassificationToIncomeAccountMappings"), // + CLASSIFICATION_CODE_VALUE_ID("classificationCodeValueId"), // ; private final String value; diff --git a/fineract-core/src/main/java/org/apache/fineract/accounting/journalentry/data/AdvancedMappingtDTO.java b/fineract-core/src/main/java/org/apache/fineract/accounting/journalentry/data/AdvancedMappingtDTO.java new file mode 100755 index 000000000..14234e6c2 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/accounting/journalentry/data/AdvancedMappingtDTO.java @@ -0,0 +1,31 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.accounting.journalentry.data; + +import java.math.BigDecimal; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class AdvancedMappingtDTO { + + private final Long referenceValueId; + private final BigDecimal amount; +} diff --git a/fineract-core/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/data/ClassificationToGLAccountData.java b/fineract-core/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/data/ClassificationToGLAccountData.java new file mode 100644 index 000000000..fcec6272c --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/data/ClassificationToGLAccountData.java @@ -0,0 +1,36 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.accounting.producttoaccountmapping.data; + +import java.io.Serializable; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.apache.fineract.accounting.glaccount.data.GLAccountData; +import org.apache.fineract.infrastructure.codes.data.CodeValueData; + +@Data +@NoArgsConstructor +@Accessors(chain = true) +public class ClassificationToGLAccountData implements Serializable { + + private static final long serialVersionUID = 1L; + private CodeValueData classificationCodeValue; + private GLAccountData incomeAccount; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/api/CodeValuesApiResourceSwagger.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/api/CodeValuesApiResourceSwagger.java similarity index 95% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/api/CodeValuesApiResourceSwagger.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/api/CodeValuesApiResourceSwagger.java index 2407ec3ba..6d79420ef 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/api/CodeValuesApiResourceSwagger.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/api/CodeValuesApiResourceSwagger.java @@ -23,7 +23,7 @@ import io.swagger.v3.oas.annotations.media.Schema; /** * Created by sanyam on 30/7/17. */ -final class CodeValuesApiResourceSwagger { +public final class CodeValuesApiResourceSwagger { private CodeValuesApiResourceSwagger() { @@ -44,6 +44,10 @@ final class CodeValuesApiResourceSwagger { public String description; @Schema(example = "0") public Integer position; + @Schema(example = "true") + public Boolean active; + @Schema(example = "false") + public Boolean mandatory; } @Schema(description = "PostCodeValuesDataRequest") diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/api/CodesApiResourceSwagger.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/api/CodesApiResourceSwagger.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/api/CodesApiResourceSwagger.java rename to fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/api/CodesApiResourceSwagger.java diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/mapper/CodeValueMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/mapper/CodeValueMapper.java new file mode 100644 index 000000000..eca38f69a --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/mapper/CodeValueMapper.java @@ -0,0 +1,36 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.codes.mapper; + +import java.util.List; +import org.apache.fineract.infrastructure.codes.data.CodeValueData; +import org.apache.fineract.infrastructure.codes.domain.CodeValue; +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapstructMapperConfig.class) +public interface CodeValueMapper { + + @Mapping(target = "name", source = "label") + CodeValueData map(CodeValue source); + + List map(List source); + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java b/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java index 44439a316..80b64277e 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java @@ -149,6 +149,30 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi updateChargeOffReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN); } + public void saveCapitalizedIncomeClassificationToIncomeAccountMappings(final JsonCommand command, final JsonElement element, + final Long productId, final Map changes) { + saveClassificationToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, + LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); + } + + public void updateCapitalizedIncomeClassificationToIncomeAccountMappings(final JsonCommand command, final JsonElement element, + final Long productId, final Map changes) { + updateClassificationToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, + LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); + } + + public void saveBuyDownFeeClassificationToIncomeAccountMappings(final JsonCommand command, final JsonElement element, + final Long productId, final Map changes) { + saveClassificationToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, + LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); + } + + public void updateBuyDownFeeClassificationToIncomeAccountMappings(final JsonCommand command, final JsonElement element, + final Long productId, final Map changes) { + updateClassificationToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, + LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); + } + public void updateChargesToIncomeAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, final Map changes) { // update both fee and penalty charges diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java index b68c9e18d..1dbb6c4e1 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java @@ -25,6 +25,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.fineract.accounting.journalentry.data.AdvancedMappingtDTO; @Getter @Setter @@ -47,5 +48,7 @@ public class AccountingBridgeDataDTO { private boolean isWrittenOff; private List newLoanTransactions = new ArrayList<>(); private boolean merchantBuyDownFee; + private List buydownFeeClassificationCodeValue; + private List capitalizedIncomeClassificationCodeValue; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMappingRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMappingRepository.java index 22ad707ba..fd355087c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMappingRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMappingRepository.java @@ -61,4 +61,13 @@ public interface LoanAmortizationAllocationMappingRepository WHERE laam.baseLoanTransactionId = :baseLoanTransactionId AND laam.loanId = :loanId """) BigDecimal calculateAlreadyAmortizedAmount(@Param("baseLoanTransactionId") Long baseLoanTransactionId, @Param("loanId") Long loanId); + + @Query(""" + SELECT laam FROM LoanAmortizationAllocationMapping laam + JOIN LoanTransaction at ON at.id = laam.baseLoanTransactionId + WHERE laam.amortizationLoanTransactionId = :amortizationLoanTransactionId + AND laam.loanId = :loanId + """) + List fetchLoanTransactionAllocationByAmortizationLoanTransactionId( + @Param("amortizationLoanTransactionId") Long amortizationLoanTransactionId, @Param("loanId") Long loanId); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java index 2fcdc83c7..4862798bf 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; +import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.loanaccount.data.CumulativeIncomeFromIncomePosting; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData; @@ -473,4 +474,11 @@ public interface LoanTransactionRepository extends JpaRepository paymentChannelToFundSourceMappings; public List feeToIncomeAccountMappings; public List chargeOffReasonToExpenseAccountMappings; + public List buydownfeeClassificationToIncomeAccountMappings; + public List capitalizedIncomeClassificationToIncomeAccountMappings; public List penaltyToIncomeAccountMappings; // Multi Disburse @@ -378,6 +381,16 @@ public final class LoanProductsApiResourceSwagger { @Schema(example = "1") public Long expenseAccountId; } + + static final class PostClassificationToIncomeAccountMappings { + + private PostClassificationToIncomeAccountMappings() {} + + @Schema(example = "1") + public Long classificationCodeValueId; + @Schema(example = "1") + public Long incomeAccountId; + } } @Schema(description = "PostLoanProductsResponse") @@ -1168,6 +1181,8 @@ public final class LoanProductsApiResourceSwagger { public List buyDownFeeCalculationTypeOptions; public List buyDownFeeStrategyOptions; public List buyDownFeeIncomeTypeOptions; + public List capitalizedIncomeClassificationOptions; + public List buydownFeeClassificationOptions; } @Schema(description = "GetLoanProductsProductIdResponse") @@ -1294,6 +1309,26 @@ public final class LoanProductsApiResourceSwagger { public Long fundSourceAccountId; } + 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 GetClassificationToIncomeAccountMappings { + + private GetClassificationToIncomeAccountMappings() {} + + public GetCodeValuesDataResponse classificationCodeValue; + public GetGLAccountData incomeAccount; + } + static final class GetChargeOffReasonToExpenseAccountMappings { private GetChargeOffReasonToExpenseAccountMappings() {} @@ -1317,18 +1352,6 @@ public final class LoanProductsApiResourceSwagger { @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 { @@ -1516,6 +1539,11 @@ public final class LoanProductsApiResourceSwagger { public List buyDownFeeCalculationTypeOptions; public List buyDownFeeStrategyOptions; public List buyDownFeeIncomeTypeOptions; + public List capitalizedIncomeClassificationOptions; + public List buydownFeeClassificationOptions; + public List buydownFeeClassificationToIncomeAccountMappings; + public List capitalizedIncomeClassificationToIncomeAccountMappings; + } @Schema(description = "PutLoanProductsProductIdRequest") @@ -1746,6 +1774,8 @@ public final class LoanProductsApiResourceSwagger { public List paymentChannelToFundSourceMappings; public List feeToIncomeAccountMappings; public List chargeOffReasonToExpenseAccountMappings; + public List buydownfeeClassificationToIncomeAccountMappings; + public List capitalizedIncomeClassificationToIncomeAccountMappings; 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 ff33b5f51..87660df6d 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 @@ -32,6 +32,7 @@ import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; +import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.core.api.ApiFacingEnum; @@ -261,6 +262,11 @@ public class LoanProductData implements Serializable { private List buyDownFeeStrategyOptions; private List buyDownFeeIncomeTypeOptions; + private final List capitalizedIncomeClassificationOptions; + private final List buydownFeeClassificationOptions; + private List capitalizedIncomeClassificationToIncomeAccountMappings; + private List buydownFeeClassificationToIncomeAccountMappings; + /** * Used when returning lookup information about loan product for dropdowns. */ @@ -822,12 +828,16 @@ public class LoanProductData implements Serializable { final Collection paymentChannelToFundSourceMappings, final Collection feeToGLAccountMappings, final Collection penaltyToGLAccountMappings, - final List chargeOffReasonToGLAccountMappings) { + final List chargeOffReasonToGLAccountMappings, + final List capitalizedIncomeClassificationToIncomeAccountMappings, + final List buydownFeeClassificationToIncomeAccountMappings) { productData.accountingMappings = accountingMappings; productData.paymentChannelToFundSourceMappings = paymentChannelToFundSourceMappings; productData.feeToIncomeAccountMappings = feeToGLAccountMappings; productData.penaltyToIncomeAccountMappings = penaltyToGLAccountMappings; productData.chargeOffReasonToExpenseAccountMappings = chargeOffReasonToGLAccountMappings; + productData.capitalizedIncomeClassificationToIncomeAccountMappings = capitalizedIncomeClassificationToIncomeAccountMappings; + productData.buydownFeeClassificationToIncomeAccountMappings = buydownFeeClassificationToIncomeAccountMappings; return productData; } @@ -1036,6 +1046,10 @@ public class LoanProductData implements Serializable { this.buyDownFeeCalculationTypeOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeCalculationType.class); this.buyDownFeeStrategyOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class); this.buyDownFeeIncomeTypeOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class); + this.capitalizedIncomeClassificationOptions = null; + this.buydownFeeClassificationOptions = null; + this.capitalizedIncomeClassificationToIncomeAccountMappings = null; + this.buydownFeeClassificationToIncomeAccountMappings = null; } public LoanProductData(final LoanProductData productData, final Collection chargeOptions, @@ -1065,7 +1079,8 @@ public class LoanProductData implements Serializable { final List capitalizedIncomeStrategyOptions, final List capitalizedIncomeTypeOptions, final List buyDownFeeCalculationTypeOptions, final List buyDownFeeStrategyOptions, - final List buyDownFeeIncomeTypeOptions) { + final List buyDownFeeIncomeTypeOptions, final List capitalizedIncomeClassificationOptions, + final List buydownFeeClassificationOptions) { this.id = productData.id; this.name = productData.name; @@ -1246,6 +1261,10 @@ public class LoanProductData implements Serializable { this.buyDownFeeIncomeTypeOptions = buyDownFeeIncomeTypeOptions; this.merchantBuyDownFee = productData.isMerchantBuyDownFee(); + this.capitalizedIncomeClassificationOptions = capitalizedIncomeClassificationOptions; + this.buydownFeeClassificationOptions = buydownFeeClassificationOptions; + this.buydownFeeClassificationToIncomeAccountMappings = productData.buydownFeeClassificationToIncomeAccountMappings; + this.capitalizedIncomeClassificationToIncomeAccountMappings = productData.capitalizedIncomeClassificationToIncomeAccountMappings; } private Collection nullIfEmpty(final Collection charges) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/handler/CreateLoanProductCommandHandler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/handler/CreateLoanProductCommandHandler.java index 72d6dbd17..98bd26866 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/handler/CreateLoanProductCommandHandler.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/handler/CreateLoanProductCommandHandler.java @@ -18,30 +18,25 @@ */ package org.apache.fineract.portfolio.loanproduct.handler; +import lombok.AllArgsConstructor; import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.portfolio.loanproduct.service.LoanProductWritePlatformService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service +@AllArgsConstructor @CommandType(entity = "LOANPRODUCT", action = "CREATE") public class CreateLoanProductCommandHandler implements NewCommandSourceHandler { private final LoanProductWritePlatformService writePlatformService; - @Autowired - public CreateLoanProductCommandHandler(final LoanProductWritePlatformService writePlatformService) { - this.writePlatformService = writePlatformService; - } - @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - return this.writePlatformService.createLoanProduct(command); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java index 925448d54..c415e2a7f 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java @@ -48,4 +48,6 @@ public class LoanDTO { private Long chargeOffReasonCodeValue; private boolean markedAsWrittenOff; private boolean merchantBuyDownFee; + private List buydownFeeAdvancedMappingData; + private List capitalizedIncomeAdvancedMappingData; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java index 8e984cede..2db1687b0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java @@ -35,6 +35,7 @@ import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsFor import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares; import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; +import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount; import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper; import org.apache.fineract.accounting.glaccount.domain.GLAccount; @@ -171,7 +172,9 @@ public class AccountingProcessorHelper { return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled, periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud, - chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff, merchantBuyDownFee); + chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff, merchantBuyDownFee, + accountingBridgeData.getBuydownFeeClassificationCodeValue(), + accountingBridgeData.getCapitalizedIncomeClassificationCodeValue()); } public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Long loanProductId, PortfolioProductType productType, @@ -179,6 +182,16 @@ public class AccountingProcessorHelper { return accountMappingRepository.findChargeOffReasonMapping(loanProductId, productType.getValue(), chargeOffReasonId); } + public ProductToGLAccountMapping getClassificationMappingByCodeValue(Long loanProductId, PortfolioProductType productType, + final Long classificationId, final String classificationType) { + if (LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue().equals(classificationType)) { + return accountMappingRepository.findBuydownFeeClassificationMapping(loanProductId, productType.getValue(), classificationId); + } else { + return accountMappingRepository.findCapitalizedIncomeClassificationMapping(loanProductId, productType.getValue(), + classificationId); + } + } + public SavingsDTO populateSavingsDtoFromMap(final Map accountingBridgeData, final boolean cashBasedAccountingEnabled, final boolean accrualBasedAccountingEnabled) { final Long loanId = (Long) accountingBridgeData.get("savingsId"); @@ -472,6 +485,14 @@ public class AccountingProcessorHelper { transactionId, transactionDate, amount); } + public void createJournalEntriesForLoan(final Office office, final String currencyCode, final Integer accountTypeToBeDebited, + final GLAccount accountToBeCredited, final Long loanProductId, final Long paymentTypeId, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + int accountTypeToDebitId = accountTypeToBeDebited; + createJournalEntriesForLoan(office, currencyCode, accountTypeToDebitId, accountToBeCredited, loanProductId, paymentTypeId, loanId, + transactionId, transactionDate, amount); + } + public void createSplitJournalEntriesForLoan(Office office, String currencyCode, List splitAccountsHolder, JournalAmountHolder totalAccountHolder, Long loanProductId, Long paymentTypeId, Long loanId, String transactionId, LocalDate transactionDate) { @@ -534,6 +555,14 @@ public class AccountingProcessorHelper { createCreditJournalEntryForLoan(office, currencyCode, creditAccount, loanId, transactionId, transactionDate, amount); } + private void createJournalEntriesForLoan(final Office office, final String currencyCode, final int accountTypeToDebitId, + final GLAccount creditAccount, final Long loanProductId, final Long paymentTypeId, final Long loanId, + final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { + final GLAccount debitAccount = getLinkedGLAccountForLoanProduct(loanProductId, accountTypeToDebitId, paymentTypeId); + createDebitJournalEntryForLoan(office, currencyCode, debitAccount, loanId, transactionId, transactionDate, amount); + createCreditJournalEntryForLoan(office, currencyCode, creditAccount, loanId, transactionId, transactionDate, amount); + } + private void createJournalEntriesForSavings(final Office office, final String currencyCode, final int accountTypeToDebitId, final int accountTypeToCreditId, final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { 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 40ebb2214..18a91d5b8 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 @@ -29,7 +29,9 @@ import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.closure.domain.GLClosure; import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan; import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; +import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; import org.apache.fineract.accounting.glaccount.domain.GLAccount; +import org.apache.fineract.accounting.journalentry.data.AdvancedMappingtDTO; import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; import org.apache.fineract.accounting.journalentry.data.GLAccountBalanceHolder; import org.apache.fineract.accounting.journalentry.data.LoanDTO; @@ -296,17 +298,57 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder(); + final List classificationCodeValues = loanDTO.getCapitalizedIncomeAdvancedMappingData(); + // interest payment final AccrualAccountsForLoan creditAccountType = isLoanWrittenOff ? AccrualAccountsForLoan.LOSSES_WRITTEN_OFF : AccrualAccountsForLoan.INCOME_FROM_CAPITALIZATION; if (MathUtil.isGreaterThanZero(interestAmount)) { - populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(), - AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + if (classificationCodeValues.isEmpty()) { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(), + AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + } else { + classificationCodeValues.stream().forEach(classificationCodeValue -> { + ProductToGLAccountMapping mapping = null; + if (classificationCodeValue.getReferenceValueId() != null) { + mapping = fetchAdvanceAccountingMappingForCodeValue(loanProductId, classificationCodeValue.getReferenceValueId(), + LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue()); + } + + if (mapping == null) { + populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, + creditAccountType.getValue(), AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), + glAccountBalanceHolder); + } else { + populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, mapping.getGlAccount(), + AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + } + }); + } } // handle fees payment if (MathUtil.isGreaterThanZero(feesAmount)) { - populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(), - AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + if (classificationCodeValues.isEmpty()) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(), + AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + } else { + classificationCodeValues.stream().forEach(classificationCodeValue -> { + ProductToGLAccountMapping mapping = null; + if (classificationCodeValue.getReferenceValueId() != null) { + mapping = fetchAdvanceAccountingMappingForCodeValue(loanProductId, classificationCodeValue.getReferenceValueId(), + LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue()); + } + + if (mapping == null) { + populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, + creditAccountType.getValue(), AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), + glAccountBalanceHolder); + } else { + populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, mapping.getGlAccount(), + AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + } + }); + } } // create credit entries @@ -327,6 +369,11 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } } + private ProductToGLAccountMapping fetchAdvanceAccountingMappingForCodeValue(final Long loanProductId, final Long codeValueId, + final String codeName) { + return helper.getClassificationMappingByCodeValue(loanProductId, PortfolioProductType.LOAN, codeValueId, codeName); + } + private void createJournalEntriesForChargeOffLoanCapitalizedIncomeAmortization(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office) { // loan properties @@ -493,17 +540,57 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder(); + final List classificationCodeValues = loanDTO.getBuydownFeeAdvancedMappingData(); + // interest payment final AccrualAccountsForLoan creditAccountType = isLoanWrittenOff ? AccrualAccountsForLoan.LOSSES_WRITTEN_OFF : AccrualAccountsForLoan.INCOME_FROM_BUY_DOWN; if (MathUtil.isGreaterThanZero(interestAmount)) { - populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(), - AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + if (classificationCodeValues.isEmpty()) { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(), + AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + } else { + classificationCodeValues.stream().forEach(classificationCodeValue -> { + ProductToGLAccountMapping mapping = null; + if (classificationCodeValue.getReferenceValueId() != null) { + mapping = fetchAdvanceAccountingMappingForCodeValue(loanProductId, classificationCodeValue.getReferenceValueId(), + LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue()); + } + + if (mapping == null) { + populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, + creditAccountType.getValue(), AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), + glAccountBalanceHolder); + } else { + populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, mapping.getGlAccount(), + AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + } + }); + } } // handle fees payment if (MathUtil.isGreaterThanZero(feesAmount)) { - populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(), - AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + if (classificationCodeValues.isEmpty()) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(), + AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + } else { + classificationCodeValues.stream().forEach(classificationCodeValue -> { + ProductToGLAccountMapping mapping = null; + if (classificationCodeValue.getReferenceValueId() != null) { + mapping = fetchAdvanceAccountingMappingForCodeValue(loanProductId, classificationCodeValue.getReferenceValueId(), + LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue()); + } + + if (mapping == null) { + populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, + creditAccountType.getValue(), AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), + glAccountBalanceHolder); + } else { + populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, mapping.getGlAccount(), + AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); + } + }); + } } // create credit entries @@ -800,6 +887,17 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } } + private void populateCreditDebitMaps(Long loanProductId, BigDecimal transactionPartAmount, Long paymentTypeId, GLAccount accountCredit, + Integer debitAccountType, GLAccountBalanceHolder glAccountBalanceHolder) { + if (MathUtil.isGreaterThanZero(transactionPartAmount)) { + // Resolve Credit + glAccountBalanceHolder.addToCredit(accountCredit, transactionPartAmount); + // Resolve Debit + GLAccount accountDebit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, debitAccountType, paymentTypeId); + glAccountBalanceHolder.addToDebit(accountDebit, transactionPartAmount); + } + } + private void createJournalEntriesForChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) { final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff(); if (isMarkedAsChargeOff) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java index 95d3ecc5a..973a27944 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java @@ -44,6 +44,7 @@ import org.apache.fineract.accounting.glaccount.service.GLAccountReadPlatformSer import org.apache.fineract.accounting.journalentry.api.JournalEntryJsonInputParams; import org.apache.fineract.accounting.journalentry.command.JournalEntryCommand; import org.apache.fineract.accounting.journalentry.command.SingleDebitOrCreditEntryCommand; +import org.apache.fineract.accounting.journalentry.data.AdvancedMappingtDTO; import org.apache.fineract.accounting.journalentry.data.ClientTransactionDTO; import org.apache.fineract.accounting.journalentry.data.LoanDTO; import org.apache.fineract.accounting.journalentry.data.SavingsDTO; @@ -61,6 +62,7 @@ import org.apache.fineract.accounting.provisioning.domain.ProvisioningEntry; import org.apache.fineract.accounting.rule.domain.AccountingRule; import org.apache.fineract.accounting.rule.domain.AccountingRuleRepository; import org.apache.fineract.accounting.rule.exception.AccountingRuleNotFoundException; +import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; import org.apache.fineract.infrastructure.configuration.service.ConfigurationReadPlatformService; import org.apache.fineract.infrastructure.core.api.JsonCommand; @@ -90,11 +92,14 @@ import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO; import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeLoanTransactionDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMapping; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMappingRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; @@ -128,6 +133,8 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa private final ConfigurationReadPlatformService configurationReadPlatformService; private final AccountingService accountingService; private final ExternalAssetOwnerRepository externalAssetOwnerRepository; + private final LoanAmortizationAllocationMappingRepository loanAmortizationAllocationMappingRepository; + private final LoanTransactionRepository loanTransactionRepository; @Transactional @Override @@ -857,11 +864,39 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa } } + List buydownFeeAdvancedMappingData = null; + List capitalizedIncomeAdvancedMappingData = null; + if (loanTransaction.isBuyDownFeeAmortization()) { + buydownFeeAdvancedMappingData = getLoanTransactionClassificationId(loanTransaction); + } else if (loanTransaction.isCapitalizedIncomeAmortization()) { + capitalizedIncomeAdvancedMappingData = getLoanTransactionClassificationId(loanTransaction); + } + return new AccountingBridgeDataDTO(loan.getId(), loan.productId(), loan.getOfficeId(), currencyCode, loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(), loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(), isAccountTransfer, wasChargedOffAtTransactionTime, loan.isFraud(), loan.fetchChargeOffReasonId(), loan.isClosedWrittenOff(), - transactions, loan.getLoanProductRelatedDetail().isMerchantBuyDownFee()); + transactions, loan.getLoanProductRelatedDetail().isMerchantBuyDownFee(), buydownFeeAdvancedMappingData, + capitalizedIncomeAdvancedMappingData); + } + + private List getLoanTransactionClassificationId(final LoanTransaction loanTransaction) { + List advancedMappingData = new ArrayList(); + if (loanTransaction.isCapitalizedIncomeAmortization() || loanTransaction.isBuyDownFeeAmortization()) { + final List loanTransactionAllocations = loanAmortizationAllocationMappingRepository + .fetchLoanTransactionAllocationByAmortizationLoanTransactionId(loanTransaction.getId(), + loanTransaction.getLoan().getId()); + loanTransactionAllocations.stream().forEach(loanTransactionAllocation -> { + final CodeValue classification = loanTransactionRepository + .fetchClassificationCodeValueByTransactionId(loanTransactionAllocation.getBaseLoanTransactionId()); + if (classification != null) { + advancedMappingData.add(new AdvancedMappingtDTO(classification.getId(), loanTransactionAllocation.getAmount())); + } else { + advancedMappingData.add(new AdvancedMappingtDTO(null, loanTransactionAllocation.getAmount())); + } + }); + } + return advancedMappingData; } /** diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java index 397e486ce..57c1a1f2a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/starter/AccountingJournalEntryConfiguration.java @@ -49,6 +49,8 @@ import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMappingRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @@ -94,12 +96,14 @@ public class AccountingJournalEntryConfiguration { FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper, CashBasedAccountingProcessorForClientTransactions accountingProcessorForClientTransactions, ConfigurationReadPlatformService configurationReadPlatformService, AccountingService accountingService, - ExternalAssetOwnerRepository externalAssetOwnerRepository) { + ExternalAssetOwnerRepository externalAssetOwnerRepository, + LoanAmortizationAllocationMappingRepository loanAmortizationAllocationMappingRepository, + LoanTransactionRepository loanTransactionRepository) { return new JournalEntryWritePlatformServiceJpaRepositoryImpl(glClosureRepository, glAccountRepository, glJournalEntryRepository, officeRepositoryWrapper, accountingProcessorForLoanFactory, accountingProcessorForSavingsFactory, accountingProcessorForSharesFactory, helper, fromApiJsonDeserializer, accountingRuleRepository, glAccountReadPlatformService, organisationCurrencyRepository, context, paymentDetailWritePlatformService, financialActivityAccountRepositoryWrapper, accountingProcessorForClientTransactions, configurationReadPlatformService, - accountingService, externalAssetOwnerRepository); + accountingService, externalAssetOwnerRepository, loanAmortizationAllocationMappingRepository, loanTransactionRepository); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java index bb3bc9a57..8ed70663e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java @@ -140,6 +140,10 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null); + this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element, + loanProductId, null); + this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element, + loanProductId, null); break; case ACCRUAL_UPFRONT: // Fall Through @@ -233,6 +237,10 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null); + this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element, + loanProductId, null); + this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element, + loanProductId, null); break; } } @@ -411,6 +419,10 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes); this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, changes); + this.loanProductToGLAccountMappingHelper.updateBuyDownFeeClassificationToIncomeAccountMappings(command, element, loanProductId, + changes); + this.loanProductToGLAccountMappingHelper.updateCapitalizedIncomeClassificationToIncomeAccountMappings(command, element, + loanProductId, changes); } return changes; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index 786a1fa7b..6c981ba69 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 @@ -46,10 +46,12 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import lombok.RequiredArgsConstructor; +import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; import org.apache.fineract.accounting.common.AccountingDropdownReadPlatformService; import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; +import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingReadPlatformService; import org.apache.fineract.commands.domain.CommandWrapper; @@ -82,6 +84,7 @@ import org.apache.fineract.portfolio.floatingrates.service.FloatingRatesReadPlat import org.apache.fineract.portfolio.fund.data.FundData; import org.apache.fineract.portfolio.fund.service.FundReadPlatformService; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; +import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeCalculationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy; @@ -352,6 +355,8 @@ public class LoanProductsApiResource { Collection feeToGLAccountMappings; Collection penaltyToGLAccountMappings; List chargeOffReasonToGLAccountMappings; + List capitalizedIncomeClassificationToGLAccountMappings; + List buydowFeeClassificationToGLAccountMappings; if (loanProduct.hasAccountingEnabled()) { accountingMappings = this.accountMappingReadPlatformService.fetchAccountMappingDetailsForLoanProduct(productId, loanProduct.getAccountingRule().getId().intValue()); @@ -362,8 +367,14 @@ public class LoanProductsApiResource { .fetchPenaltyToIncomeAccountMappingsForLoanProduct(productId); chargeOffReasonToGLAccountMappings = this.accountMappingReadPlatformService .fetchChargeOffReasonMappingsForLoanProduct(productId); + capitalizedIncomeClassificationToGLAccountMappings = accountMappingReadPlatformService + .fetchClassificationMappingsForLoanProduct(productId, + LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); + buydowFeeClassificationToGLAccountMappings = accountMappingReadPlatformService.fetchClassificationMappingsForLoanProduct( + productId, LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); loanProduct = LoanProductData.withAccountingDetails(loanProduct, accountingMappings, paymentChannelToFundSourceMappings, - feeToGLAccountMappings, penaltyToGLAccountMappings, chargeOffReasonToGLAccountMappings); + feeToGLAccountMappings, penaltyToGLAccountMappings, chargeOffReasonToGLAccountMappings, + capitalizedIncomeClassificationToGLAccountMappings, buydowFeeClassificationToGLAccountMappings); } if (settings.isTemplate()) { @@ -464,6 +475,10 @@ public class LoanProductsApiResource { .getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class); final List buyDownFeeIncomeTypeOptions = ApiFacingEnum .getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class); + final List capitalizedIncomeClassificationOptions = codeValueReadPlatformService + .retrieveCodeValuesByCode(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE); + final List buydownFeeClassificationOptions = codeValueReadPlatformService + .retrieveCodeValuesByCode(LoanTransactionApiConstants.BUY_DOWN_FEE_CLASSIFICATION_CODE); return new LoanProductData(productData, chargeOptions, penaltyOptions, paymentTypeOptions, currencyOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, interestRateFrequencyTypeOptions, @@ -477,7 +492,8 @@ public class LoanProductsApiResource { LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), creditAllocationTransactionTypes, creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions, daysInYearCustomStrategyOptions, capitalizedIncomeCalculationTypeOptions, capitalizedIncomeStrategyOptions, - capitalizedIncomeTypeOptions, buyDownFeeCalculationTypeOptions, buyDownFeeStrategyOptions, buyDownFeeIncomeTypeOptions); + capitalizedIncomeTypeOptions, buyDownFeeCalculationTypeOptions, buyDownFeeStrategyOptions, buyDownFeeIncomeTypeOptions, + capitalizedIncomeClassificationOptions, buydownFeeClassificationOptions); } } 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 3371368a7..cd99af21d 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 @@ -49,6 +49,7 @@ import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; +import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeCalculationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy; @@ -202,7 +203,10 @@ public final class LoanProductDataValidator { LoanProductConstants.ENABLE_BUY_DOWN_FEE_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_CALCULATION_TYPE_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_STRATEGY_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME, LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), LoanProductAccountingParams.INCOME_FROM_BUY_DOWN.getValue(), - LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME)); + LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME, + LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue(), // + LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue() // + )); private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, @@ -744,7 +748,10 @@ public final class LoanProductDataValidator { validatePaymentChannelFundSourceMappings(baseDataValidator, element); validateChargeToIncomeAccountMappings(baseDataValidator, element); validateChargeOffToExpenseMappings(baseDataValidator, element); - + validateClassificationToIncomeMappings(baseDataValidator, element, + LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); + validateClassificationToIncomeMappings(baseDataValidator, element, + LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); } if (AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) { @@ -1877,6 +1884,10 @@ public final class LoanProductDataValidator { validatePaymentChannelFundSourceMappings(baseDataValidator, element); validateChargeToIncomeAccountMappings(baseDataValidator, element); validateChargeOffToExpenseMappings(baseDataValidator, element); + validateClassificationToIncomeMappings(baseDataValidator, element, + LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); + validateClassificationToIncomeMappings(baseDataValidator, element, + LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); validateMinMaxConstraints(element, baseDataValidator, loanProduct); @@ -2109,6 +2120,65 @@ public final class LoanProductDataValidator { } } + private void validateClassificationToIncomeMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element, + final LoanProductAccountingParams classificationParameter) { + String parameterName = classificationParameter.getValue(); + + if (this.fromApiJsonHelper.parameterExists(parameterName, element)) { + final JsonArray classificationToIncomeMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element); + if (classificationToIncomeMappingArray != null && classificationToIncomeMappingArray.size() > 0) { + Map> classificationToAccounts = new HashMap<>(); + List processedMappings = new ArrayList<>(); // Collect processed mappings for the new method + + int i = 0; + do { + final JsonObject jsonObject = classificationToIncomeMappingArray.get(i).getAsJsonObject(); + final Long incomeGlAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), jsonObject); + final Long classificationCodeValueId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.CLASSIFICATION_CODE_VALUE_ID.getValue(), jsonObject); + + // Validate parameters locally + baseDataValidator.reset() + .parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT + + LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue()) + .value(incomeGlAccountId).notNull().integerGreaterThanZero(); + baseDataValidator.reset() + .parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT + + LoanProductAccountingParams.CLASSIFICATION_CODE_VALUE_ID.getValue()) + .value(classificationCodeValueId).notNull().integerGreaterThanZero(); + + // Handle duplicate classification and GL Account validation + classificationToAccounts.putIfAbsent(classificationCodeValueId, new HashSet<>()); + Set associatedAccounts = classificationToAccounts.get(classificationCodeValueId); + + if (associatedAccounts.contains(incomeGlAccountId)) { + baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET) + .failWithCode("duplicate.classification.and.glAccount"); + } + associatedAccounts.add(incomeGlAccountId); + + if (associatedAccounts.size() > 1) { + baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET) + .failWithCode("multiple.glAccounts.for.classification"); + } + + // Collect mapping for additional validations + processedMappings.add(jsonObject); + + i++; + } while (i < classificationToIncomeMappingArray.size()); + + // Call the new validation method for additional checks + final String dataCodeName = classificationParameter + .equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS) + ? LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE + : LoanTransactionApiConstants.BUY_DOWN_FEE_CLASSIFICATION_CODE; + productToGLAccountMappingHelper.validateClassificationMappingsInDatabase(processedMappings, dataCodeName); + } + } + } + public void validateMinMaxConstraints(final JsonElement element, final DataValidatorBuilder baseDataValidator, final LoanProduct loanProduct) { validatePrincipalMinMaxConstraint(element, loanProduct, baseDataValidator); 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 2cd6e8dea..7b12aaab7 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 @@ -216,4 +216,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0198_add_classification_id_to_acc_product_mapping.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0198_add_classification_id_to_acc_product_mapping.xml new file mode 100644 index 000000000..8eb87510c --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0198_add_classification_id_to_acc_product_mapping.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java b/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java index 52a3331f6..05faf8f60 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java @@ -73,7 +73,7 @@ class CreateJournalEntriesForChargeOffLoanTest { Collections.emptyList(), false, "", null, null, null, null); loanDTO = new LoanDTO(1L, 1L, 1L, "USD", false, true, true, List.of(loanTransactionDTO), false, false, chargeOffReasonId, false, - false); + false, null, null); } @Test diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java index e36fb7806..92ab6434f 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java @@ -28,28 +28,38 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.BuyDownFeeAmortizationDetails; +import org.apache.fineract.client.models.GetCodesResponse; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactions; +import org.apache.fineract.client.models.PostClassificationToIncomeAccountMappings; import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PostCodeValueDataResponse; +import org.apache.fineract.client.models.PostCodeValuesDataRequest; import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; import org.apache.fineract.client.models.PostLoansResponse; +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.ClientHelper; import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; +import org.apache.fineract.integrationtests.common.accounting.AccountHelper; import org.apache.fineract.integrationtests.common.externalevents.BusinessEvent; import org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper; import org.apache.fineract.integrationtests.common.externalevents.LoanAdjustTransactionBusinessEvent; import org.apache.fineract.integrationtests.common.externalevents.LoanTransactionMinimalBusinessEvent; +import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -88,7 +98,7 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest { runAt("01 September 2024", () -> { clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); final PostLoanProductsResponse loanProductsResponse = loanProductHelper - .createLoanProduct(createProgressiveLoanProductWithBuyDownFee()); + .createLoanProduct(createProgressiveLoanProductWithBuyDownFee(null)); // Apply for the loan with proper progressive loan settings PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, @@ -240,14 +250,16 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest { /** * Creates a progressive loan product with buy down fee enabled */ - private PostLoanProductsRequest createProgressiveLoanProductWithBuyDownFee() { + private PostLoanProductsRequest createProgressiveLoanProductWithBuyDownFee( + PostClassificationToIncomeAccountMappings buydownFeeClassificationAccountMappings) { // Create a progressive loan product with accrual-based accounting and proper GL mappings - return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("BUY_DOWN_FEE_PROGRESSIVE_", 6)) - .shortName(Utils.uniqueRandomStringGenerator("", 4)).description("Progressive loan product with buy down fee enabled") - .includeInBorrowerCycle(false).useBorrowerCycle(false).currencyCode("USD").digitsAfterDecimal(2).principal(1000.0) - .minPrincipal(100.0).maxPrincipal(10000.0).numberOfRepayments(12).minNumberOfRepayments(6).maxNumberOfRepayments(24) - .repaymentEvery(1).repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L).interestRatePerPeriod(10.0) - .minInterestRatePerPeriod(0.0).maxInterestRatePerPeriod(120.0).interestRateFrequencyType(InterestRateFrequencyType.YEARS) + PostLoanProductsRequest postLoanProductsRequest = new PostLoanProductsRequest() + .name(Utils.uniqueRandomStringGenerator("BUY_DOWN_FEE_PROGRESSIVE_", 6)).shortName(Utils.uniqueRandomStringGenerator("", 4)) + .description("Progressive loan product with buy down fee enabled").includeInBorrowerCycle(false).useBorrowerCycle(false) + .currencyCode("USD").digitsAfterDecimal(2).principal(1000.0).minPrincipal(100.0).maxPrincipal(10000.0) + .numberOfRepayments(12).minNumberOfRepayments(6).maxNumberOfRepayments(24).repaymentEvery(1) + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L).interestRatePerPeriod(10.0).minInterestRatePerPeriod(0.0) + .maxInterestRatePerPeriod(120.0).interestRateFrequencyType(InterestRateFrequencyType.YEARS) .amortizationType(AmortizationType.EQUAL_INSTALLMENTS).interestType(InterestType.DECLINING_BALANCE) .interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).allowPartialPeriodInterestCalcualtion(false) .transactionProcessingStrategyCode("advanced-payment-allocation-strategy") @@ -275,6 +287,11 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest { .buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT) .buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION) .buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE).locale("en").dateFormat("dd MMMM yyyy"); + + if (buydownFeeClassificationAccountMappings != null) { + postLoanProductsRequest.addBuydownfeeClassificationToIncomeAccountMappingsItem(buydownFeeClassificationAccountMappings); + } + return postLoanProductsRequest; } @Test @@ -485,6 +502,15 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest { return response.getResourceId(); } + private Long addBuyDownFeeForLoan(Long loanId, Double amount, String date, Long classificationId) { + String buyDownFeeExternalId = UUID.randomUUID().toString(); + PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeLoanBuyDownFee(loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(date).locale("en") + .transactionAmount(amount).externalId(buyDownFeeExternalId).note("Buy Down Fee Transaction") + .classificationId(classificationId)); + return response.getResourceId(); + } + @Test public void testBuyDownFeeDailyAmortization() { final AtomicReference loanIdRef = new AtomicReference<>(); @@ -645,7 +671,7 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest { @Test public void testRetrieveBuyDownFeeAmortizationDetails() { final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); - final PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(createProgressiveLoanProductWithBuyDownFee()); + final PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(createProgressiveLoanProductWithBuyDownFee(null)); final long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 February 2024", 1000.0, 7.0, 6, null); @@ -677,7 +703,7 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest { public void testRetrieveBuyDownFeeAmortizationDetails_notEnabled() { final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); final PostLoanProductsResponse loanProduct = loanProductHelper - .createLoanProduct(createProgressiveLoanProductWithBuyDownFee().enableBuyDownFee(false)); + .createLoanProduct(createProgressiveLoanProductWithBuyDownFee(null).enableBuyDownFee(false)); final long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 February 2024", 1000.0, 7.0, 6, null); @@ -872,7 +898,7 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest { // Add initial buy down fee final PostLoanProductsResponse loanProductsResponse = loanProductHelper - .createLoanProduct(createProgressiveLoanProductWithBuyDownFee().merchantBuyDownFee(false)); + .createLoanProduct(createProgressiveLoanProductWithBuyDownFee(null).merchantBuyDownFee(false)); // Apply for the loan with proper progressive loan settings PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, @@ -917,4 +943,90 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest { }); } + @Test + public void testBuyDownFeeWithAdvanceAccountingMappings() { + final AtomicReference loanIdRef = new AtomicReference<>(); + final AtomicReference classificationIdRef = new AtomicReference<>(); + final AtomicReference classificationIncomeAccountRef = new AtomicReference<>(); + runAt("10 September 2024", () -> { + deleteAllExternalEvents(); + + final AccountHelper accountHelper = new AccountHelper(this.requestSpec, this.responseSpec); + final Account classificationIncomeAccount = accountHelper + .createIncomeAccount(Utils.uniqueRandomStringGenerator("buydownfee_class_income_", 6)); + classificationIncomeAccountRef.set(classificationIncomeAccount); + + final GetCodesResponse code = codeHelper.retrieveCodeByName(LoanTransactionApiConstants.BUY_DOWN_FEE_CLASSIFICATION_CODE); + final PostCodeValueDataResponse classificationCode = codeHelper.createCodeValue(code.getId(), + new PostCodeValuesDataRequest().name(Utils.uniqueRandomStringGenerator("CLASS_", 6)).isActive(true).position(10)); + classificationIdRef.set(classificationCode.getSubResourceId()); + + // Loan Product create + final PostClassificationToIncomeAccountMappings classificationToIncomeMapping = new PostClassificationToIncomeAccountMappings() + .classificationCodeValueId(classificationIdRef.get()) + .incomeAccountId(classificationIncomeAccount.getAccountID().longValue()); + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(createProgressiveLoanProductWithBuyDownFee(classificationToIncomeMapping)); + + GetLoanProductsProductIdResponse getLoanProductResponse = loanProductHelper + .retrieveLoanProductById(loanProductsResponse.getResourceId()); + assertNotNull(getLoanProductResponse); + assertNotNull(getLoanProductResponse.getBuydownFeeClassificationToIncomeAccountMappings()); + Assertions.assertEquals(1, getLoanProductResponse.getBuydownFeeClassificationToIncomeAccountMappings().size()); + Assertions.assertEquals(classificationIdRef.get(), getLoanProductResponse.getBuydownFeeClassificationToIncomeAccountMappings() + .get(0).getClassificationCodeValue().getId()); + + final PostCodeValueDataResponse secClassificationCode = codeHelper.createCodeValue(code.getId(), + new PostCodeValuesDataRequest().name(Utils.uniqueRandomStringGenerator("CLASS_", 6)).isActive(true).position(10)); + classificationIdRef.set(secClassificationCode.getSubResourceId()); + + // Loan Product update + final PutLoanProductsProductIdRequest putLoanProductRequest = new PutLoanProductsProductIdRequest(); + putLoanProductRequest.addBuydownfeeClassificationToIncomeAccountMappingsItem( + new PostClassificationToIncomeAccountMappings().classificationCodeValueId(classificationIdRef.get()) + .incomeAccountId(classificationIncomeAccount.getAccountID().longValue())); + + loanProductHelper.updateLoanProductById(loanProductsResponse.getResourceId(), putLoanProductRequest); + getLoanProductResponse = loanProductHelper.retrieveLoanProductById(loanProductsResponse.getResourceId()); + assertNotNull(getLoanProductResponse); + assertNotNull(getLoanProductResponse.getBuydownFeeClassificationToIncomeAccountMappings()); + Assertions.assertEquals(1, getLoanProductResponse.getBuydownFeeClassificationToIncomeAccountMappings().size()); + Assertions.assertEquals(classificationIdRef.get(), getLoanProductResponse.getBuydownFeeClassificationToIncomeAccountMappings() + .get(0).getClassificationCodeValue().getId()); + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, + loanProductsResponse.getResourceId(), "10 September 2024", 1000.0, 10.0, 12, null)); + loanId = postLoansResponse.getLoanId(); + loanIdRef.set(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "10 September 2024")); + disburseLoan(loanId, BigDecimal.valueOf(1000.0), "10 September 2024"); + + Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 400.0, "10 September 2024", classificationIdRef.get()); + assertNotNull(buyDownFeeTransactionId); + }); + + runAt("20 September 2024", () -> { + Long loanId = loanIdRef.get(); + deleteAllExternalEvents(); + executeInlineCOB(loanId); + + Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 50.0, "20 September 2024"); + assertNotNull(buyDownFeeTransactionId); + }); + + runAt("30 September 2024", () -> { + Long loanId = loanIdRef.get(); + deleteAllExternalEvents(); + executeInlineCOB(loanId); + + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + final Optional optTx = loanDetails.getTransactions().stream() + .filter(item -> Objects.equals(Utils.getDoubleValue(item.getAmount()), 1.23) + && Objects.equals(item.getType().getValue(), "Buy Down Fee Amortization")) + .findFirst(); + verifyTRJournalEntries(optTx.get().getId(), debit(deferredIncomeLiabilityAccount, 1.23), + credit(classificationIncomeAccountRef.get(), 1.09), // First BuyDown Fee With classification + credit(feeIncomeAccount, 0.14)); // Second BuyDown Fee Without classification + }); + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java index e0e1ef1ee..01b16f443 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java @@ -26,14 +26,17 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import org.apache.fineract.client.models.CapitalizedIncomeDetails; import org.apache.fineract.client.models.GetCodesResponse; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactions; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.LoanCapitalizedIncomeData; +import org.apache.fineract.client.models.PostClassificationToIncomeAccountMappings; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostCodeValueDataResponse; import org.apache.fineract.client.models.PostCodeValuesDataRequest; @@ -42,10 +45,13 @@ import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansResponse; +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.ClientHelper; import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; +import org.apache.fineract.integrationtests.common.accounting.AccountHelper; import org.apache.fineract.integrationtests.common.externalevents.LoanAdjustTransactionBusinessEvent; import org.apache.fineract.integrationtests.common.externalevents.LoanBusinessEvent; import org.apache.fineract.integrationtests.common.externalevents.LoanTransactionBusinessEvent; @@ -1179,4 +1185,103 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest { ); }); } + + @Test + public void testCapitalizedIncomeWithAdvanceAccountingMappings() { + final AtomicReference loanIdRef = new AtomicReference<>(); + final AtomicReference classificationIdRef = new AtomicReference<>(); + final AtomicReference classificationIncomeAccountRef = new AtomicReference<>(); + runAt("10 September 2024", () -> { + deleteAllExternalEvents(); + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + + final AccountHelper accountHelper = new AccountHelper(this.requestSpec, this.responseSpec); + final Account classificationIncomeAccount = accountHelper + .createIncomeAccount(Utils.uniqueRandomStringGenerator("capitalizedincome_class_income_", 6)); + classificationIncomeAccountRef.set(classificationIncomeAccount); + + final GetCodesResponse code = codeHelper.retrieveCodeByName(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE); + final PostCodeValueDataResponse classificationCode = codeHelper.createCodeValue(code.getId(), + new PostCodeValuesDataRequest().name(Utils.uniqueRandomStringGenerator("CLASS_", 6)).isActive(true).position(10)); + classificationIdRef.set(classificationCode.getSubResourceId()); + + // Loan Product create + final PostClassificationToIncomeAccountMappings classificationToIncomeMapping = new PostClassificationToIncomeAccountMappings() + .classificationCodeValueId(classificationIdRef.get()) + .incomeAccountId(classificationIncomeAccount.getAccountID().longValue()); + + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().enableIncomeCapitalization(true) + .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT) + .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION) + .deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue()) + .incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue()) + .capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE) + .addCapitalizedIncomeClassificationToIncomeAccountMappingsItem(classificationToIncomeMapping)); + + GetLoanProductsProductIdResponse getLoanProductResponse = loanProductHelper + .retrieveLoanProductById(loanProductsResponse.getResourceId()); + assertNotNull(getLoanProductResponse); + assertNotNull(getLoanProductResponse.getCapitalizedIncomeClassificationToIncomeAccountMappings()); + Assertions.assertEquals(1, getLoanProductResponse.getCapitalizedIncomeClassificationToIncomeAccountMappings().size()); + Assertions.assertEquals(classificationIdRef.get(), getLoanProductResponse + .getCapitalizedIncomeClassificationToIncomeAccountMappings().get(0).getClassificationCodeValue().getId()); + + final PostCodeValueDataResponse secClassificationCode = codeHelper.createCodeValue(code.getId(), + new PostCodeValuesDataRequest().name(Utils.uniqueRandomStringGenerator("CLASS_", 6)).isActive(true).position(10)); + classificationIdRef.set(secClassificationCode.getSubResourceId()); + + // Loan Product update + final PutLoanProductsProductIdRequest putLoanProductRequest = new PutLoanProductsProductIdRequest(); + putLoanProductRequest.addCapitalizedIncomeClassificationToIncomeAccountMappingsItem( + new PostClassificationToIncomeAccountMappings().classificationCodeValueId(classificationIdRef.get()) + .incomeAccountId(classificationIncomeAccount.getAccountID().longValue())); + + loanProductHelper.updateLoanProductById(loanProductsResponse.getResourceId(), putLoanProductRequest); + getLoanProductResponse = loanProductHelper.retrieveLoanProductById(loanProductsResponse.getResourceId()); + assertNotNull(getLoanProductResponse); + assertNotNull(getLoanProductResponse.getCapitalizedIncomeClassificationToIncomeAccountMappings()); + Assertions.assertEquals(1, getLoanProductResponse.getCapitalizedIncomeClassificationToIncomeAccountMappings().size()); + Assertions.assertEquals(classificationIdRef.get(), getLoanProductResponse + .getCapitalizedIncomeClassificationToIncomeAccountMappings().get(0).getClassificationCodeValue().getId()); + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + loanProductsResponse.getResourceId(), "10 September 2024", 1000.0, 10.0, 12, null)); + Long loanId = postLoansResponse.getLoanId(); + loanIdRef.set(loanId); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "10 September 2024")); + disburseLoan(loanId, BigDecimal.valueOf(1000.0), "10 September 2024"); + + Long capitalizedIncomeTransactionId = loanTransactionHelper + .addCapitalizedIncome(loanId, "10 September 2024", 100.0, classificationIdRef.get()).getResourceId(); + assertNotNull(capitalizedIncomeTransactionId); + }); + + runAt("20 September 2024", () -> { + Long loanId = loanIdRef.get(); + deleteAllExternalEvents(); + executeInlineCOB(loanId); + + Long capitalizedIncomeTransactionId = loanTransactionHelper.addCapitalizedIncome(loanId, "20 September 2024", 20.0) + .getResourceId(); + assertNotNull(capitalizedIncomeTransactionId); + }); + + runAt("30 September 2024", () -> { + Long loanId = loanIdRef.get(); + deleteAllExternalEvents(); + executeInlineCOB(loanId); + + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + final Optional optTx = loanDetails.getTransactions().stream() + .filter(item -> Objects.equals(Utils.getDoubleValue(item.getAmount()), 0.33) + && Objects.equals(item.getType().getValue(), "Capitalized Income Amortization")) + .findFirst(); + verifyTRJournalEntries(optTx.get().getId(), debit(deferredIncomeLiabilityAccount, 0.33), + credit(classificationIncomeAccountRef.get(), 0.27), // First Capitalized Income With classification + credit(feeIncomeAccount, 0.06)); // Second Capitalized Income Without classification + + }); + } + }