This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit cb70555485ad401b6c88d708aa2e60c9c5c6dafe Author: Jose Alberto Hernandez <[email protected]> AuthorDate: Mon Sep 8 21:05:22 2025 -0500 FINERACT-2374: Advance Accounting rule for classification type --- .../domain/ProductToGLAccountMapping.java | 14 +- .../ProductToGLAccountMappingRepository.java | 21 ++- .../service/ProductToGLAccountMappingHelper.java | 160 +++++++++++++++++++++ ...oductToGLAccountMappingReadPlatformService.java | 5 + ...tToGLAccountMappingReadPlatformServiceImpl.java | 51 +++++-- .../accounting/common/AccountingConstants.java | 7 +- .../journalentry/data/AdvancedMappingtDTO.java | 32 +---- .../data/ClassificationToGLAccountData.java | 36 +++++ .../codes/api/CodeValuesApiResourceSwagger.java | 6 +- .../codes/api/CodesApiResourceSwagger.java | 0 .../codes/mapper/CodeValueMapper.java | 41 ++---- .../LoanProductToGLAccountMappingHelper.java | 24 ++++ .../loanaccount/data/AccountingBridgeDataDTO.java | 3 + ...oanAmortizationAllocationMappingRepository.java | 9 ++ .../domain/LoanTransactionRepository.java | 8 ++ .../api/LoanProductsApiResourceSwagger.java | 54 +++++-- .../loanproduct/data/LoanProductData.java | 23 ++- .../handler/CreateLoanProductCommandHandler.java | 9 +- .../accounting/journalentry/data/LoanDTO.java | 2 + .../service/AccountingProcessorHelper.java | 31 +++- .../AccrualBasedAccountingProcessorForLoan.java | 114 +++++++++++++-- ...EntryWritePlatformServiceJpaRepositoryImpl.java | 37 ++++- .../AccountingJournalEntryConfiguration.java | 8 +- ...ToGLAccountMappingWritePlatformServiceImpl.java | 12 ++ .../loanproduct/api/LoanProductsApiResource.java | 20 ++- .../serialization/LoanProductDataValidator.java | 74 +++++++++- .../db/changelog/tenant/changelog-tenant.xml | 1 + ...dd_classification_id_to_acc_product_mapping.xml | 52 +++++++ .../CreateJournalEntriesForChargeOffLoanTest.java | 2 +- .../integrationtests/LoanBuyDownFeeTest.java | 134 +++++++++++++++-- .../LoanCapitalizedIncomeTest.java | 105 ++++++++++++++ 31 files changed, 975 insertions(+), 120 deletions(-) diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java index 1f602e90d5..12edabfae4 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<Long> { @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 2b2bc21172..917fbdffd7 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<ProductToGLAccountMapping> 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<ProductToGLAccountMapping> 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<ProductToGLAccountMapping> 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<ProductToGLAccountMapping> 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 dcd9ec44ca..b37580c872 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<String, Object> 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<String, Object> changes, final PortfolioProductType portfolioProductType, + final LoanProductAccountingParams classificationParameter) { + + final List<ProductToGLAccountMapping> 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<Long, Long> inputClassificationToGLAccountMap = new HashMap<>(); + + final Set<Long> 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> 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<Long, Long> 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> 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<CodeValue> 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<GLAccountType> getAllowedAccountTypesForFeeMapping() { List<GLAccountType> allowedAccountTypes = new ArrayList<>(); allowedAccountTypes.add(GLAccountType.INCOME); @@ -610,4 +736,38 @@ public class ProductToGLAccountMappingHelper { throw new PlatformApiDataValidationException(validationErrors); } } + + public void validateClassificationMappingsInDatabase(final List<JsonObject> mappings, final String dataCodeName) { + final List<ApiParameterError> 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> 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 6c89ab1a3f..22ccc37c13 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<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForShareProduct(Long productId); List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId); + + List<ClassificationToGLAccountData> 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 6e2c44c237..1fcb7bf6d1 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<String, Object> 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<ClassificationToGLAccountData> fetchClassificationMappings(final PortfolioProductType portfolioProductType, + final Long loanProductId, LoanProductAccountingParams classificationParameter) { + final List<ProductToGLAccountMapping> 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<ClassificationToGLAccountData> 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<String, Object> fetchAccountMappingDetailsForShareProduct(Long productId, Integer accountingType) { @@ -344,6 +367,12 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product return fetchChargeOffReasonMappings(PortfolioProductType.LOAN, loanProductId); } + @Override + public List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId, + LoanProductAccountingParams classificationParameter) { + return fetchClassificationMappings(PortfolioProductType.LOAN, loanProductId, classificationParameter); + } + private Map<String, Object> setAccrualPeriodicSavingsProductToGLAccountMaps(final List<ProductToGLAccountMapping> mappings) { final Map<String, Object> 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 1c50780e88..7435a46d90 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-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java b/fineract-core/src/main/java/org/apache/fineract/accounting/journalentry/data/AdvancedMappingtDTO.java similarity index 53% copy from fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java copy to fineract-core/src/main/java/org/apache/fineract/accounting/journalentry/data/AdvancedMappingtDTO.java index 925448d541..14234e6c23 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java +++ b/fineract-core/src/main/java/org/apache/fineract/accounting/journalentry/data/AdvancedMappingtDTO.java @@ -18,34 +18,14 @@ */ package org.apache.fineract.accounting.journalentry.data; -import java.util.List; -import lombok.AllArgsConstructor; +import java.math.BigDecimal; import lombok.Getter; -import lombok.Setter; +import lombok.RequiredArgsConstructor; -@AllArgsConstructor +@RequiredArgsConstructor @Getter -public class LoanDTO { +public class AdvancedMappingtDTO { - @Setter - private Long loanId; - @Setter - private Long loanProductId; - @Setter - private Long officeId; - @Setter - private String currencyCode; - @Setter - private boolean cashBasedAccountingEnabled; - private final boolean upfrontAccrualBasedAccountingEnabled; - private final boolean periodicAccrualBasedAccountingEnabled; - @Setter - private List<LoanTransactionDTO> newLoanTransactions; - @Setter - private boolean markedAsChargeOff; - @Setter - private boolean markedAsFraud; - private Long chargeOffReasonCodeValue; - private boolean markedAsWrittenOff; - private boolean merchantBuyDownFee; + 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 0000000000..fcec6272cd --- /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 2407ec3ba4..6d79420ef7 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-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/mapper/CodeValueMapper.java old mode 100755 new mode 100644 similarity index 50% copy from fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java copy to fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/mapper/CodeValueMapper.java index 925448d541..eca38f69a0 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/codes/mapper/CodeValueMapper.java @@ -16,36 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.accounting.journalentry.data; +package org.apache.fineract.infrastructure.codes.mapper; import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; +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; -@AllArgsConstructor -@Getter -public class LoanDTO { +@Mapper(config = MapstructMapperConfig.class) +public interface CodeValueMapper { + + @Mapping(target = "name", source = "label") + CodeValueData map(CodeValue source); + + List<CodeValueData> map(List<CodeValue> source); - @Setter - private Long loanId; - @Setter - private Long loanProductId; - @Setter - private Long officeId; - @Setter - private String currencyCode; - @Setter - private boolean cashBasedAccountingEnabled; - private final boolean upfrontAccrualBasedAccountingEnabled; - private final boolean periodicAccrualBasedAccountingEnabled; - @Setter - private List<LoanTransactionDTO> newLoanTransactions; - @Setter - private boolean markedAsChargeOff; - @Setter - private boolean markedAsFraud; - private Long chargeOffReasonCodeValue; - private boolean markedAsWrittenOff; - private boolean merchantBuyDownFee; } 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 44439a316f..80b64277e4 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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 b68c9e18dd..1dbb6c4e17 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<AccountingBridgeLoanTransactionDTO> newLoanTransactions = new ArrayList<>(); private boolean merchantBuyDownFee; + private List<AdvancedMappingtDTO> buydownFeeClassificationCodeValue; + private List<AdvancedMappingtDTO> 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 22ad707baa..fd355087c6 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<LoanAmortizationAllocationMapping> 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 2fcdc83c7b..4862798bf7 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<LoanTransaction boolean existsNonReversedByLoanAndTypeAndDate(@Param("loan") Loan loan, @Param("type") LoanTransactionType type, @Param("transactionDate") LocalDate transactionDate); + @Query(""" + SELECT lt.classification + FROM LoanTransaction lt + WHERE lt.id = :transactionId + """) + CodeValue fetchClassificationCodeValueByTransactionId(@Param("transactionId") Long transactionId); + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java index 00fc321aeb..ccb47b2ffc 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java @@ -24,6 +24,7 @@ import java.time.LocalDate; import java.util.List; import java.util.Set; import org.apache.fineract.accounting.glaccount.data.GLAccountData; +import org.apache.fineract.infrastructure.codes.api.CodeValuesApiResourceSwagger.GetCodeValuesDataResponse; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; @@ -300,6 +301,8 @@ public final class LoanProductsApiResourceSwagger { public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings; public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings; public List<PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings; + public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> buydownfeeClassificationToIncomeAccountMappings; + public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings; public List<LoanProductChargeToGLAccountMapper> 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<StringEnumOptionData> buyDownFeeCalculationTypeOptions; public List<StringEnumOptionData> buyDownFeeStrategyOptions; public List<StringEnumOptionData> buyDownFeeIncomeTypeOptions; + public List<GetCodeValuesDataResponse> capitalizedIncomeClassificationOptions; + public List<GetCodeValuesDataResponse> 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<StringEnumOptionData> buyDownFeeCalculationTypeOptions; public List<StringEnumOptionData> buyDownFeeStrategyOptions; public List<StringEnumOptionData> buyDownFeeIncomeTypeOptions; + public List<GetCodeValuesDataResponse> capitalizedIncomeClassificationOptions; + public List<GetCodeValuesDataResponse> buydownFeeClassificationOptions; + public List<GetClassificationToIncomeAccountMappings> buydownFeeClassificationToIncomeAccountMappings; + public List<GetClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings; + } @Schema(description = "PutLoanProductsProductIdRequest") @@ -1746,6 +1774,8 @@ public final class LoanProductsApiResourceSwagger { public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings; public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings; public List<PostLoanProductsRequest.PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings; + public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> buydownfeeClassificationToIncomeAccountMappings; + public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings; public List<LoanProductChargeToGLAccountMapper> 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 ff33b5f513..87660df6d0 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<StringEnumOptionData> buyDownFeeStrategyOptions; private List<StringEnumOptionData> buyDownFeeIncomeTypeOptions; + private final List<CodeValueData> capitalizedIncomeClassificationOptions; + private final List<CodeValueData> buydownFeeClassificationOptions; + private List<ClassificationToGLAccountData> capitalizedIncomeClassificationToIncomeAccountMappings; + private List<ClassificationToGLAccountData> buydownFeeClassificationToIncomeAccountMappings; + /** * Used when returning lookup information about loan product for dropdowns. */ @@ -822,12 +828,16 @@ public class LoanProductData implements Serializable { final Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings, final Collection<ChargeToGLAccountMapper> feeToGLAccountMappings, final Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings, - final List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings) { + final List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings, + final List<ClassificationToGLAccountData> capitalizedIncomeClassificationToIncomeAccountMappings, + final List<ClassificationToGLAccountData> 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<ChargeData> chargeOptions, @@ -1065,7 +1079,8 @@ public class LoanProductData implements Serializable { final List<StringEnumOptionData> capitalizedIncomeStrategyOptions, final List<StringEnumOptionData> capitalizedIncomeTypeOptions, final List<StringEnumOptionData> buyDownFeeCalculationTypeOptions, final List<StringEnumOptionData> buyDownFeeStrategyOptions, - final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions) { + final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions, final List<CodeValueData> capitalizedIncomeClassificationOptions, + final List<CodeValueData> 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<ChargeData> nullIfEmpty(final Collection<ChargeData> 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 72d6dbd174..98bd268664 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 925448d541..c415e2a7f2 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<AdvancedMappingtDTO> buydownFeeAdvancedMappingData; + private List<AdvancedMappingtDTO> 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 8e984cede1..2db1687b09 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<String, Object> 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<JournalAmountHolder> 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 40ebb2214f..18a91d5b8d 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<AdvancedMappingtDTO> 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<AdvancedMappingtDTO> 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 95d3ecc5a8..973a279441 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<AdvancedMappingtDTO> buydownFeeAdvancedMappingData = null; + List<AdvancedMappingtDTO> 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<AdvancedMappingtDTO> getLoanTransactionClassificationId(final LoanTransaction loanTransaction) { + List<AdvancedMappingtDTO> advancedMappingData = new ArrayList<AdvancedMappingtDTO>(); + if (loanTransaction.isCapitalizedIncomeAmortization() || loanTransaction.isBuyDownFeeAmortization()) { + final List<LoanAmortizationAllocationMapping> 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 397e486cef..57c1a1f2a0 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 bb3bc9a578..8ed70663e7 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 786a1fa7b6..6c981ba69a 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<ChargeToGLAccountMapper> feeToGLAccountMappings; Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings; List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings; + List<ClassificationToGLAccountData> capitalizedIncomeClassificationToGLAccountMappings; + List<ClassificationToGLAccountData> 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<StringEnumOptionData> buyDownFeeIncomeTypeOptions = ApiFacingEnum .getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class); + final List<CodeValueData> capitalizedIncomeClassificationOptions = codeValueReadPlatformService + .retrieveCodeValuesByCode(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE); + final List<CodeValueData> 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 3371368a78..cd99af21d7 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<Long, Set<Long>> classificationToAccounts = new HashMap<>(); + List<JsonObject> 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<Long> 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 2cd6e8dea7..7b12aaab7d 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 @@ <include file="parts/0195_create_loan_amortization_allocation_mapping.xml" relativeToChangelogFile="true" /> <include file="parts/0196_add_deleted_and_closed_to_buy_down_fee_balance.xml" relativeToChangelogFile="true" /> <include file="parts/0197_add_deleted_and_closed_to_capitalized_income_balance.xml" relativeToChangelogFile="true" /> + <include file="parts/0198_add_classification_id_to_acc_product_mapping.xml" relativeToChangelogFile="true" /> </databaseChangeLog> 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 0000000000..8eb87510c8 --- /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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd"> + + <changeSet id="1" author="fineract"> + <addColumn tableName="acc_product_mapping"> + <column name="capitalized_income_classification_id" type="INT" defaultValueNumeric="NULL"> + <constraints nullable="true"/> + </column> + </addColumn> + <addForeignKeyConstraint + baseTableName="acc_product_mapping" + baseColumnNames="capitalized_income_classification_id" + referencedTableName="m_code_value" + referencedColumnNames="id" + constraintName="fk_acc_product_mapping_capitalized_income_classification"/> + </changeSet> + <changeSet id="2" author="fineract"> + <addColumn tableName="acc_product_mapping"> + <column name="buydown_fee_classification_id" type="INT" defaultValueNumeric="NULL"> + <constraints nullable="true"/> + </column> + </addColumn> + <addForeignKeyConstraint + baseTableName="acc_product_mapping" + baseColumnNames="buydown_fee_classification_id" + referencedTableName="m_code_value" + referencedColumnNames="id" + constraintName="fk_acc_product_mapping_buydown_fee_classification"/> + </changeSet> +</databaseChangeLog> 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 52a3331f62..05faf8f603 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 e36fb78060..92ab6434f2 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<Long> 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<Long> loanIdRef = new AtomicReference<>(); + final AtomicReference<Long> classificationIdRef = new AtomicReference<>(); + final AtomicReference<Account> 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<GetLoansLoanIdTransactions> 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 e0e1ef1ee0..01b16f443e 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<Long> loanIdRef = new AtomicReference<>(); + final AtomicReference<Long> classificationIdRef = new AtomicReference<>(); + final AtomicReference<Account> 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<GetLoansLoanIdTransactions> 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 + + }); + } + }
