FINERACT-2374: Advance Accounting rule for classification type

This commit is contained in:
Jose Alberto Hernandez
2025-09-30 12:27:45 +02:00
committed by Adam Saghy
parent 6d3e5fa150
commit cb70555485
31 changed files with 1023 additions and 66 deletions
@@ -68,10 +68,20 @@ public class ProductToGLAccountMapping extends AbstractPersistableCustom<Long> {
@JoinColumn(name = "charge_off_reason_id", nullable = true) @JoinColumn(name = "charge_off_reason_id", nullable = true)
private CodeValue chargeOffReason; 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, 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) return new ProductToGLAccountMapping().setGlAccount(glAccount).setProductId(productId).setProductType(productType)
.setFinancialAccountType(financialAccountType).setChargeOffReason(chargeOffReason); .setFinancialAccountType(financialAccountType).setChargeOffReason(chargeOffReason)
.setCapitalizedIncomeClassification(capitalizedIncomeClassification).setBuydownFeeClassification(buydownFeeClassification);
} }
} }
@@ -35,7 +35,7 @@ public interface ProductToGLAccountMappingRepository
@Param("productType") int productType, @Param("financialAccountType") int financialAccountType, @Param("productType") int productType, @Param("financialAccountType") int financialAccountType,
@Param("chargeId") Long ChargeId); @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, ProductToGLAccountMapping findCoreProductToFinAccountMapping(@Param("productId") Long productId, @Param("productType") int productType,
@Param("financialAccountType") int financialAccountType); @Param("financialAccountType") int financialAccountType);
@@ -70,7 +70,7 @@ public interface ProductToGLAccountMappingRepository
ProductToGLAccountMapping findChargeOffReasonMapping(@Param("productId") Long productId, @Param("productType") Integer productType, ProductToGLAccountMapping findChargeOffReasonMapping(@Param("productId") Long productId, @Param("productType") Integer productType,
@Param("chargeOffReasonId") Long chargeOffReasonId); @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); 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") @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") @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); 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);
} }
@@ -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 command
* @param element * @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 * @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() { private List<GLAccountType> getAllowedAccountTypesForFeeMapping() {
List<GLAccountType> allowedAccountTypes = new ArrayList<>(); List<GLAccountType> allowedAccountTypes = new ArrayList<>();
allowedAccountTypes.add(GLAccountType.INCOME); allowedAccountTypes.add(GLAccountType.INCOME);
@@ -610,4 +736,38 @@ public class ProductToGLAccountMappingHelper {
throw new PlatformApiDataValidationException(validationErrors); 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);
}
}
} }
@@ -20,8 +20,10 @@ package org.apache.fineract.accounting.producttoaccountmapping.service;
import java.util.List; import java.util.List;
import java.util.Map; 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.ChargeOffReasonToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; 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.data.PaymentTypeToGLAccountMapper;
public interface ProductToGLAccountMappingReadPlatformService { public interface ProductToGLAccountMappingReadPlatformService {
@@ -49,4 +51,7 @@ public interface ProductToGLAccountMappingReadPlatformService {
List<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForShareProduct(Long productId); List<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForShareProduct(Long productId);
List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId); List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId);
List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId,
LoanProductAccountingParams classificationParameter);
} }
@@ -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.CashAccountsForSavings;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares;
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingDataParams; 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.SavingProductAccountingDataParams;
import org.apache.fineract.accounting.common.AccountingConstants.SharesProductAccountingParams; import org.apache.fineract.accounting.common.AccountingConstants.SharesProductAccountingParams;
import org.apache.fineract.accounting.common.AccountingRuleType; 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.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; 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.data.PaymentTypeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping; import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.apache.fineract.infrastructure.codes.data.CodeValueData; 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.PortfolioProductType;
import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.data.ChargeData;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Slf4j @Slf4j
@@ -52,9 +54,8 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor @RequiredArgsConstructor
public class ProductToGLAccountMappingReadPlatformServiceImpl implements ProductToGLAccountMappingReadPlatformService { public class ProductToGLAccountMappingReadPlatformServiceImpl implements ProductToGLAccountMappingReadPlatformService {
private final JdbcTemplate jdbcTemplate;
private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository; private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository;
private final CodeValueMapper codeValueMapper;
@Override @Override
public Map<String, Object> fetchAccountMappingDetailsForLoanProduct(final Long loanProductId, final Integer accountingType) { 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 glAccountName = mapping.getGlAccount().getName();
final String glCode = mapping.getGlAccount().getGlCode(); final String glCode = mapping.getGlAccount().getGlCode();
final GLAccountData chargeOffExpenseAccount = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode); final GLAccountData chargeOffExpenseAccount = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode);
final Long chargeOffReasonId = mapping.getChargeOffReason().getId(); final CodeValueData chargeOffReasonsCodeValue = codeValueMapper.map(mapping.getChargeOffReason());
final String codeValue = mapping.getChargeOffReason().getLabel();
final String codeDescription = mapping.getChargeOffReason().getDescription();
final Integer orderPosition = mapping.getChargeOffReason().getPosition();
final boolean isActive = mapping.getChargeOffReason().isActive();
final boolean isMandatory = mapping.getChargeOffReason().isMandatory();
final CodeValueData chargeOffReasonsCodeValue = CodeValueData.builder().id(chargeOffReasonId).name(codeValue)
.description(codeDescription).position(orderPosition).active(isActive).mandatory(isMandatory).build();
final ChargeOffReasonToGLAccountMapper chargeOffReasonToGLAccountMapper = new ChargeOffReasonToGLAccountMapper() final ChargeOffReasonToGLAccountMapper chargeOffReasonToGLAccountMapper = new ChargeOffReasonToGLAccountMapper()
.setChargeOffReasonCodeValue(chargeOffReasonsCodeValue).setExpenseAccount(chargeOffExpenseAccount); .setChargeOffReasonCodeValue(chargeOffReasonsCodeValue).setExpenseAccount(chargeOffExpenseAccount);
@@ -299,6 +293,35 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
return chargeOffReasonToGLAccountMappers; 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 @Override
public Map<String, Object> fetchAccountMappingDetailsForShareProduct(Long productId, Integer accountingType) { public Map<String, Object> fetchAccountMappingDetailsForShareProduct(Long productId, Integer accountingType) {
@@ -344,6 +367,12 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
return fetchChargeOffReasonMappings(PortfolioProductType.LOAN, loanProductId); 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) { private Map<String, Object> setAccrualPeriodicSavingsProductToGLAccountMaps(final List<ProductToGLAccountMapping> mappings) {
final Map<String, Object> accountMappingDetails = new LinkedHashMap<>(8); final Map<String, Object> accountMappingDetails = new LinkedHashMap<>(8);
@@ -53,7 +53,9 @@ public final class AccountingConstants {
INCOME_FROM_CHARGE_OFF_PENALTY(18), // INCOME_FROM_CHARGE_OFF_PENALTY(18), //
INCOME_FROM_GOODWILL_CREDIT_INTEREST(19), // INCOME_FROM_GOODWILL_CREDIT_INTEREST(19), //
INCOME_FROM_GOODWILL_CREDIT_FEES(20), // 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; private final Integer value;
@@ -185,6 +187,9 @@ public final class AccountingConstants {
INCOME_FROM_CAPITALIZATION("incomeFromCapitalizationAccountId"), // INCOME_FROM_CAPITALIZATION("incomeFromCapitalizationAccountId"), //
BUY_DOWN_EXPENSE("buyDownExpenseAccountId"), // BUY_DOWN_EXPENSE("buyDownExpenseAccountId"), //
INCOME_FROM_BUY_DOWN("incomeFromBuyDownAccountId"), // 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; private final String value;
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.journalentry.data;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public class AdvancedMappingtDTO {
private final Long referenceValueId;
private final BigDecimal amount;
}
@@ -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;
}
@@ -23,7 +23,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
/** /**
* Created by sanyam on 30/7/17. * Created by sanyam on 30/7/17.
*/ */
final class CodeValuesApiResourceSwagger { public final class CodeValuesApiResourceSwagger {
private CodeValuesApiResourceSwagger() { private CodeValuesApiResourceSwagger() {
@@ -44,6 +44,10 @@ final class CodeValuesApiResourceSwagger {
public String description; public String description;
@Schema(example = "0") @Schema(example = "0")
public Integer position; public Integer position;
@Schema(example = "true")
public Boolean active;
@Schema(example = "false")
public Boolean mandatory;
} }
@Schema(description = "PostCodeValuesDataRequest") @Schema(description = "PostCodeValuesDataRequest")
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.codes.mapper;
import java.util.List;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(config = MapstructMapperConfig.class)
public interface CodeValueMapper {
@Mapping(target = "name", source = "label")
CodeValueData map(CodeValue source);
List<CodeValueData> map(List<CodeValue> source);
}
@@ -149,6 +149,30 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
updateChargeOffReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN); 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, public void updateChargesToIncomeAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
final Map<String, Object> changes) { final Map<String, Object> changes) {
// update both fee and penalty charges // update both fee and penalty charges
@@ -25,6 +25,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.fineract.accounting.journalentry.data.AdvancedMappingtDTO;
@Getter @Getter
@Setter @Setter
@@ -47,5 +48,7 @@ public class AccountingBridgeDataDTO {
private boolean isWrittenOff; private boolean isWrittenOff;
private List<AccountingBridgeLoanTransactionDTO> newLoanTransactions = new ArrayList<>(); private List<AccountingBridgeLoanTransactionDTO> newLoanTransactions = new ArrayList<>();
private boolean merchantBuyDownFee; private boolean merchantBuyDownFee;
private List<AdvancedMappingtDTO> buydownFeeClassificationCodeValue;
private List<AdvancedMappingtDTO> capitalizedIncomeClassificationCodeValue;
} }
@@ -61,4 +61,13 @@ public interface LoanAmortizationAllocationMappingRepository
WHERE laam.baseLoanTransactionId = :baseLoanTransactionId AND laam.loanId = :loanId WHERE laam.baseLoanTransactionId = :baseLoanTransactionId AND laam.loanId = :loanId
""") """)
BigDecimal calculateAlreadyAmortizedAmount(@Param("baseLoanTransactionId") Long baseLoanTransactionId, @Param("loanId") Long 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);
} }
@@ -24,6 +24,7 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.loanaccount.data.CumulativeIncomeFromIncomePosting; import org.apache.fineract.portfolio.loanaccount.data.CumulativeIncomeFromIncomePosting;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData; 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, boolean existsNonReversedByLoanAndTypeAndDate(@Param("loan") Loan loan, @Param("type") LoanTransactionType type,
@Param("transactionDate") LocalDate transactionDate); @Param("transactionDate") LocalDate transactionDate);
@Query("""
SELECT lt.classification
FROM LoanTransaction lt
WHERE lt.id = :transactionId
""")
CodeValue fetchClassificationCodeValueByTransactionId(@Param("transactionId") Long transactionId);
} }
@@ -24,6 +24,7 @@ import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.apache.fineract.accounting.glaccount.data.GLAccountData; 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.EnumOptionData;
import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; import org.apache.fineract.infrastructure.core.data.StringEnumOptionData;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
@@ -300,6 +301,8 @@ public final class LoanProductsApiResourceSwagger {
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings; public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings; public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings;
public List<PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings; public List<PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings;
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> buydownfeeClassificationToIncomeAccountMappings;
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings;
public List<LoanProductChargeToGLAccountMapper> penaltyToIncomeAccountMappings; public List<LoanProductChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
// Multi Disburse // Multi Disburse
@@ -378,6 +381,16 @@ public final class LoanProductsApiResourceSwagger {
@Schema(example = "1") @Schema(example = "1")
public Long expenseAccountId; public Long expenseAccountId;
} }
static final class PostClassificationToIncomeAccountMappings {
private PostClassificationToIncomeAccountMappings() {}
@Schema(example = "1")
public Long classificationCodeValueId;
@Schema(example = "1")
public Long incomeAccountId;
}
} }
@Schema(description = "PostLoanProductsResponse") @Schema(description = "PostLoanProductsResponse")
@@ -1168,6 +1181,8 @@ public final class LoanProductsApiResourceSwagger {
public List<StringEnumOptionData> buyDownFeeCalculationTypeOptions; public List<StringEnumOptionData> buyDownFeeCalculationTypeOptions;
public List<StringEnumOptionData> buyDownFeeStrategyOptions; public List<StringEnumOptionData> buyDownFeeStrategyOptions;
public List<StringEnumOptionData> buyDownFeeIncomeTypeOptions; public List<StringEnumOptionData> buyDownFeeIncomeTypeOptions;
public List<GetCodeValuesDataResponse> capitalizedIncomeClassificationOptions;
public List<GetCodeValuesDataResponse> buydownFeeClassificationOptions;
} }
@Schema(description = "GetLoanProductsProductIdResponse") @Schema(description = "GetLoanProductsProductIdResponse")
@@ -1294,6 +1309,26 @@ public final class LoanProductsApiResourceSwagger {
public Long fundSourceAccountId; 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 { static final class GetChargeOffReasonToExpenseAccountMappings {
private GetChargeOffReasonToExpenseAccountMappings() {} private GetChargeOffReasonToExpenseAccountMappings() {}
@@ -1317,18 +1352,6 @@ public final class LoanProductsApiResourceSwagger {
@Schema(example = "false") @Schema(example = "false")
public Boolean mandatory; 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 { static final class GetLoanFeeToIncomeAccountMappings {
@@ -1516,6 +1539,11 @@ public final class LoanProductsApiResourceSwagger {
public List<StringEnumOptionData> buyDownFeeCalculationTypeOptions; public List<StringEnumOptionData> buyDownFeeCalculationTypeOptions;
public List<StringEnumOptionData> buyDownFeeStrategyOptions; public List<StringEnumOptionData> buyDownFeeStrategyOptions;
public List<StringEnumOptionData> buyDownFeeIncomeTypeOptions; public List<StringEnumOptionData> buyDownFeeIncomeTypeOptions;
public List<GetCodeValuesDataResponse> capitalizedIncomeClassificationOptions;
public List<GetCodeValuesDataResponse> buydownFeeClassificationOptions;
public List<GetClassificationToIncomeAccountMappings> buydownFeeClassificationToIncomeAccountMappings;
public List<GetClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings;
} }
@Schema(description = "PutLoanProductsProductIdRequest") @Schema(description = "PutLoanProductsProductIdRequest")
@@ -1746,6 +1774,8 @@ public final class LoanProductsApiResourceSwagger {
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings; public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings; public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings;
public List<PostLoanProductsRequest.PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings; public List<PostLoanProductsRequest.PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings;
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> buydownfeeClassificationToIncomeAccountMappings;
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings;
public List<LoanProductChargeToGLAccountMapper> penaltyToIncomeAccountMappings; public List<LoanProductChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
@Schema(example = "false") @Schema(example = "false")
public Boolean enableAccrualActivityPosting; public Boolean enableAccrualActivityPosting;
@@ -32,6 +32,7 @@ import org.apache.fineract.accounting.common.AccountingRuleType;
import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; 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.data.PaymentTypeToGLAccountMapper;
import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.api.ApiFacingEnum; import org.apache.fineract.infrastructure.core.api.ApiFacingEnum;
@@ -261,6 +262,11 @@ public class LoanProductData implements Serializable {
private List<StringEnumOptionData> buyDownFeeStrategyOptions; private List<StringEnumOptionData> buyDownFeeStrategyOptions;
private List<StringEnumOptionData> buyDownFeeIncomeTypeOptions; 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. * 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<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings,
final Collection<ChargeToGLAccountMapper> feeToGLAccountMappings, final Collection<ChargeToGLAccountMapper> feeToGLAccountMappings,
final Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings, final Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings,
final List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings) { final List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings,
final List<ClassificationToGLAccountData> capitalizedIncomeClassificationToIncomeAccountMappings,
final List<ClassificationToGLAccountData> buydownFeeClassificationToIncomeAccountMappings) {
productData.accountingMappings = accountingMappings; productData.accountingMappings = accountingMappings;
productData.paymentChannelToFundSourceMappings = paymentChannelToFundSourceMappings; productData.paymentChannelToFundSourceMappings = paymentChannelToFundSourceMappings;
productData.feeToIncomeAccountMappings = feeToGLAccountMappings; productData.feeToIncomeAccountMappings = feeToGLAccountMappings;
productData.penaltyToIncomeAccountMappings = penaltyToGLAccountMappings; productData.penaltyToIncomeAccountMappings = penaltyToGLAccountMappings;
productData.chargeOffReasonToExpenseAccountMappings = chargeOffReasonToGLAccountMappings; productData.chargeOffReasonToExpenseAccountMappings = chargeOffReasonToGLAccountMappings;
productData.capitalizedIncomeClassificationToIncomeAccountMappings = capitalizedIncomeClassificationToIncomeAccountMappings;
productData.buydownFeeClassificationToIncomeAccountMappings = buydownFeeClassificationToIncomeAccountMappings;
return productData; return productData;
} }
@@ -1036,6 +1046,10 @@ public class LoanProductData implements Serializable {
this.buyDownFeeCalculationTypeOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeCalculationType.class); this.buyDownFeeCalculationTypeOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeCalculationType.class);
this.buyDownFeeStrategyOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class); this.buyDownFeeStrategyOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class);
this.buyDownFeeIncomeTypeOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.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, 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> capitalizedIncomeStrategyOptions,
final List<StringEnumOptionData> capitalizedIncomeTypeOptions, final List<StringEnumOptionData> capitalizedIncomeTypeOptions,
final List<StringEnumOptionData> buyDownFeeCalculationTypeOptions, final List<StringEnumOptionData> buyDownFeeStrategyOptions, 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.id = productData.id;
this.name = productData.name; this.name = productData.name;
@@ -1246,6 +1261,10 @@ public class LoanProductData implements Serializable {
this.buyDownFeeIncomeTypeOptions = buyDownFeeIncomeTypeOptions; this.buyDownFeeIncomeTypeOptions = buyDownFeeIncomeTypeOptions;
this.merchantBuyDownFee = productData.isMerchantBuyDownFee(); 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) { private Collection<ChargeData> nullIfEmpty(final Collection<ChargeData> charges) {
@@ -18,30 +18,25 @@
*/ */
package org.apache.fineract.portfolio.loanproduct.handler; package org.apache.fineract.portfolio.loanproduct.handler;
import lombok.AllArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType; import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductWritePlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Service @Service
@AllArgsConstructor
@CommandType(entity = "LOANPRODUCT", action = "CREATE") @CommandType(entity = "LOANPRODUCT", action = "CREATE")
public class CreateLoanProductCommandHandler implements NewCommandSourceHandler { public class CreateLoanProductCommandHandler implements NewCommandSourceHandler {
private final LoanProductWritePlatformService writePlatformService; private final LoanProductWritePlatformService writePlatformService;
@Autowired
public CreateLoanProductCommandHandler(final LoanProductWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult processCommand(final JsonCommand command) { public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createLoanProduct(command); return this.writePlatformService.createLoanProduct(command);
} }
} }
@@ -48,4 +48,6 @@ public class LoanDTO {
private Long chargeOffReasonCodeValue; private Long chargeOffReasonCodeValue;
private boolean markedAsWrittenOff; private boolean markedAsWrittenOff;
private boolean merchantBuyDownFee; private boolean merchantBuyDownFee;
private List<AdvancedMappingtDTO> buydownFeeAdvancedMappingData;
private List<AdvancedMappingtDTO> capitalizedIncomeAdvancedMappingData;
} }
@@ -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.CashAccountsForSavings;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; 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.FinancialActivityAccount;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper; import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.accounting.glaccount.domain.GLAccount;
@@ -171,7 +172,9 @@ public class AccountingProcessorHelper {
return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled, return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled,
periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud, periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud,
chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff, merchantBuyDownFee); chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff, merchantBuyDownFee,
accountingBridgeData.getBuydownFeeClassificationCodeValue(),
accountingBridgeData.getCapitalizedIncomeClassificationCodeValue());
} }
public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Long loanProductId, PortfolioProductType productType, public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Long loanProductId, PortfolioProductType productType,
@@ -179,6 +182,16 @@ public class AccountingProcessorHelper {
return accountMappingRepository.findChargeOffReasonMapping(loanProductId, productType.getValue(), chargeOffReasonId); 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, public SavingsDTO populateSavingsDtoFromMap(final Map<String, Object> accountingBridgeData, final boolean cashBasedAccountingEnabled,
final boolean accrualBasedAccountingEnabled) { final boolean accrualBasedAccountingEnabled) {
final Long loanId = (Long) accountingBridgeData.get("savingsId"); final Long loanId = (Long) accountingBridgeData.get("savingsId");
@@ -472,6 +485,14 @@ public class AccountingProcessorHelper {
transactionId, transactionDate, amount); 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, public void createSplitJournalEntriesForLoan(Office office, String currencyCode, List<JournalAmountHolder> splitAccountsHolder,
JournalAmountHolder totalAccountHolder, Long loanProductId, Long paymentTypeId, Long loanId, String transactionId, JournalAmountHolder totalAccountHolder, Long loanProductId, Long paymentTypeId, Long loanId, String transactionId,
LocalDate transactionDate) { LocalDate transactionDate) {
@@ -534,6 +555,14 @@ public class AccountingProcessorHelper {
createCreditJournalEntryForLoan(office, currencyCode, creditAccount, loanId, transactionId, transactionDate, amount); 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, 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 int accountTypeToCreditId, final Long savingsProductId, final Long paymentTypeId, final Long savingsId,
final String transactionId, final LocalDate transactionDate, final BigDecimal amount) { final String transactionId, final LocalDate transactionDate, final BigDecimal amount) {
@@ -29,7 +29,9 @@ import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.domain.GLClosure; import org.apache.fineract.accounting.closure.domain.GLClosure;
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan; import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; 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.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.ChargePaymentDTO;
import org.apache.fineract.accounting.journalentry.data.GLAccountBalanceHolder; import org.apache.fineract.accounting.journalentry.data.GLAccountBalanceHolder;
import org.apache.fineract.accounting.journalentry.data.LoanDTO; import org.apache.fineract.accounting.journalentry.data.LoanDTO;
@@ -296,17 +298,57 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder(); final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
final List<AdvancedMappingtDTO> classificationCodeValues = loanDTO.getCapitalizedIncomeAdvancedMappingData();
// interest payment // interest payment
final AccrualAccountsForLoan creditAccountType = isLoanWrittenOff ? AccrualAccountsForLoan.LOSSES_WRITTEN_OFF final AccrualAccountsForLoan creditAccountType = isLoanWrittenOff ? AccrualAccountsForLoan.LOSSES_WRITTEN_OFF
: AccrualAccountsForLoan.INCOME_FROM_CAPITALIZATION; : AccrualAccountsForLoan.INCOME_FROM_CAPITALIZATION;
if (MathUtil.isGreaterThanZero(interestAmount)) { if (MathUtil.isGreaterThanZero(interestAmount)) {
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(), if (classificationCodeValues.isEmpty()) {
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); 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 // handle fees payment
if (MathUtil.isGreaterThanZero(feesAmount)) { if (MathUtil.isGreaterThanZero(feesAmount)) {
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(), if (classificationCodeValues.isEmpty()) {
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); 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 // 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, private void createJournalEntriesForChargeOffLoanCapitalizedIncomeAmortization(final LoanDTO loanDTO,
final LoanTransactionDTO loanTransactionDTO, final Office office) { final LoanTransactionDTO loanTransactionDTO, final Office office) {
// loan properties // loan properties
@@ -493,17 +540,57 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder(); final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
final List<AdvancedMappingtDTO> classificationCodeValues = loanDTO.getBuydownFeeAdvancedMappingData();
// interest payment // interest payment
final AccrualAccountsForLoan creditAccountType = isLoanWrittenOff ? AccrualAccountsForLoan.LOSSES_WRITTEN_OFF final AccrualAccountsForLoan creditAccountType = isLoanWrittenOff ? AccrualAccountsForLoan.LOSSES_WRITTEN_OFF
: AccrualAccountsForLoan.INCOME_FROM_BUY_DOWN; : AccrualAccountsForLoan.INCOME_FROM_BUY_DOWN;
if (MathUtil.isGreaterThanZero(interestAmount)) { if (MathUtil.isGreaterThanZero(interestAmount)) {
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(), if (classificationCodeValues.isEmpty()) {
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); 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 // handle fees payment
if (MathUtil.isGreaterThanZero(feesAmount)) { if (MathUtil.isGreaterThanZero(feesAmount)) {
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(), if (classificationCodeValues.isEmpty()) {
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder); 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 // 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) { private void createJournalEntriesForChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) {
final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff(); final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff();
if (isMarkedAsChargeOff) { if (isMarkedAsChargeOff) {
@@ -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.api.JournalEntryJsonInputParams;
import org.apache.fineract.accounting.journalentry.command.JournalEntryCommand; import org.apache.fineract.accounting.journalentry.command.JournalEntryCommand;
import org.apache.fineract.accounting.journalentry.command.SingleDebitOrCreditEntryCommand; 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.ClientTransactionDTO;
import org.apache.fineract.accounting.journalentry.data.LoanDTO; import org.apache.fineract.accounting.journalentry.data.LoanDTO;
import org.apache.fineract.accounting.journalentry.data.SavingsDTO; 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.AccountingRule;
import org.apache.fineract.accounting.rule.domain.AccountingRuleRepository; import org.apache.fineract.accounting.rule.domain.AccountingRuleRepository;
import org.apache.fineract.accounting.rule.exception.AccountingRuleNotFoundException; 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.api.GlobalConfigurationConstants;
import org.apache.fineract.infrastructure.configuration.service.ConfigurationReadPlatformService; import org.apache.fineract.infrastructure.configuration.service.ConfigurationReadPlatformService;
import org.apache.fineract.infrastructure.core.api.JsonCommand; 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.AccountingBridgeLoanTransactionDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; 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.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; 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.loanproduct.service.LoanEnumerations;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
@@ -128,6 +133,8 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
private final ConfigurationReadPlatformService configurationReadPlatformService; private final ConfigurationReadPlatformService configurationReadPlatformService;
private final AccountingService accountingService; private final AccountingService accountingService;
private final ExternalAssetOwnerRepository externalAssetOwnerRepository; private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
private final LoanAmortizationAllocationMappingRepository loanAmortizationAllocationMappingRepository;
private final LoanTransactionRepository loanTransactionRepository;
@Transactional @Transactional
@Override @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, return new AccountingBridgeDataDTO(loan.getId(), loan.productId(), loan.getOfficeId(), currencyCode,
loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(), loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(), loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
isAccountTransfer, wasChargedOffAtTransactionTime, loan.isFraud(), loan.fetchChargeOffReasonId(), loan.isClosedWrittenOff(), 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;
} }
/** /**
@@ -49,6 +49,8 @@ import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService; import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; 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.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -94,12 +96,14 @@ public class AccountingJournalEntryConfiguration {
FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper, FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper,
CashBasedAccountingProcessorForClientTransactions accountingProcessorForClientTransactions, CashBasedAccountingProcessorForClientTransactions accountingProcessorForClientTransactions,
ConfigurationReadPlatformService configurationReadPlatformService, AccountingService accountingService, ConfigurationReadPlatformService configurationReadPlatformService, AccountingService accountingService,
ExternalAssetOwnerRepository externalAssetOwnerRepository) { ExternalAssetOwnerRepository externalAssetOwnerRepository,
LoanAmortizationAllocationMappingRepository loanAmortizationAllocationMappingRepository,
LoanTransactionRepository loanTransactionRepository) {
return new JournalEntryWritePlatformServiceJpaRepositoryImpl(glClosureRepository, glAccountRepository, glJournalEntryRepository, return new JournalEntryWritePlatformServiceJpaRepositoryImpl(glClosureRepository, glAccountRepository, glJournalEntryRepository,
officeRepositoryWrapper, accountingProcessorForLoanFactory, accountingProcessorForSavingsFactory, officeRepositoryWrapper, accountingProcessorForLoanFactory, accountingProcessorForSavingsFactory,
accountingProcessorForSharesFactory, helper, fromApiJsonDeserializer, accountingRuleRepository, accountingProcessorForSharesFactory, helper, fromApiJsonDeserializer, accountingRuleRepository,
glAccountReadPlatformService, organisationCurrencyRepository, context, paymentDetailWritePlatformService, glAccountReadPlatformService, organisationCurrencyRepository, context, paymentDetailWritePlatformService,
financialActivityAccountRepositoryWrapper, accountingProcessorForClientTransactions, configurationReadPlatformService, financialActivityAccountRepositoryWrapper, accountingProcessorForClientTransactions, configurationReadPlatformService,
accountingService, externalAssetOwnerRepository); accountingService, externalAssetOwnerRepository, loanAmortizationAllocationMappingRepository, loanTransactionRepository);
} }
} }
@@ -140,6 +140,10 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(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; break;
case ACCRUAL_UPFRONT: case ACCRUAL_UPFRONT:
// Fall Through // Fall Through
@@ -233,6 +237,10 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(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; break;
} }
} }
@@ -411,6 +419,10 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes); this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes);
this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId,
changes); changes);
this.loanProductToGLAccountMappingHelper.updateBuyDownFeeClassificationToIncomeAccountMappings(command, element, loanProductId,
changes);
this.loanProductToGLAccountMappingHelper.updateCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
loanProductId, changes);
} }
return changes; return changes;
} }
@@ -46,10 +46,12 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
import org.apache.fineract.accounting.common.AccountingDropdownReadPlatformService; import org.apache.fineract.accounting.common.AccountingDropdownReadPlatformService;
import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; 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.data.PaymentTypeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingReadPlatformService; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingReadPlatformService;
import org.apache.fineract.commands.domain.CommandWrapper; 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.data.FundData;
import org.apache.fineract.portfolio.fund.service.FundReadPlatformService; import org.apache.fineract.portfolio.fund.service.FundReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; 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.LoanBuyDownFeeCalculationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy;
@@ -352,6 +355,8 @@ public class LoanProductsApiResource {
Collection<ChargeToGLAccountMapper> feeToGLAccountMappings; Collection<ChargeToGLAccountMapper> feeToGLAccountMappings;
Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings; Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings;
List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings; List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings;
List<ClassificationToGLAccountData> capitalizedIncomeClassificationToGLAccountMappings;
List<ClassificationToGLAccountData> buydowFeeClassificationToGLAccountMappings;
if (loanProduct.hasAccountingEnabled()) { if (loanProduct.hasAccountingEnabled()) {
accountingMappings = this.accountMappingReadPlatformService.fetchAccountMappingDetailsForLoanProduct(productId, accountingMappings = this.accountMappingReadPlatformService.fetchAccountMappingDetailsForLoanProduct(productId,
loanProduct.getAccountingRule().getId().intValue()); loanProduct.getAccountingRule().getId().intValue());
@@ -362,8 +367,14 @@ public class LoanProductsApiResource {
.fetchPenaltyToIncomeAccountMappingsForLoanProduct(productId); .fetchPenaltyToIncomeAccountMappingsForLoanProduct(productId);
chargeOffReasonToGLAccountMappings = this.accountMappingReadPlatformService chargeOffReasonToGLAccountMappings = this.accountMappingReadPlatformService
.fetchChargeOffReasonMappingsForLoanProduct(productId); .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, loanProduct = LoanProductData.withAccountingDetails(loanProduct, accountingMappings, paymentChannelToFundSourceMappings,
feeToGLAccountMappings, penaltyToGLAccountMappings, chargeOffReasonToGLAccountMappings); feeToGLAccountMappings, penaltyToGLAccountMappings, chargeOffReasonToGLAccountMappings,
capitalizedIncomeClassificationToGLAccountMappings, buydowFeeClassificationToGLAccountMappings);
} }
if (settings.isTemplate()) { if (settings.isTemplate()) {
@@ -464,6 +475,10 @@ public class LoanProductsApiResource {
.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class); .getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class);
final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions = ApiFacingEnum final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions = ApiFacingEnum
.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class); .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, return new LoanProductData(productData, chargeOptions, penaltyOptions, paymentTypeOptions, currencyOptions, amortizationTypeOptions,
interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, interestRateFrequencyTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, interestRateFrequencyTypeOptions,
@@ -477,7 +492,8 @@ public class LoanProductsApiResource {
LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), creditAllocationTransactionTypes, LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), creditAllocationTransactionTypes,
creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions, creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions,
daysInYearCustomStrategyOptions, capitalizedIncomeCalculationTypeOptions, capitalizedIncomeStrategyOptions, daysInYearCustomStrategyOptions, capitalizedIncomeCalculationTypeOptions, capitalizedIncomeStrategyOptions,
capitalizedIncomeTypeOptions, buyDownFeeCalculationTypeOptions, buyDownFeeStrategyOptions, buyDownFeeIncomeTypeOptions); capitalizedIncomeTypeOptions, buyDownFeeCalculationTypeOptions, buyDownFeeStrategyOptions, buyDownFeeIncomeTypeOptions,
capitalizedIncomeClassificationOptions, buydownFeeClassificationOptions);
} }
} }
@@ -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.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; 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.LoanBuyDownFeeCalculationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy; 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.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, 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(), 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, private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName,
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName,
@@ -744,7 +748,10 @@ public final class LoanProductDataValidator {
validatePaymentChannelFundSourceMappings(baseDataValidator, element); validatePaymentChannelFundSourceMappings(baseDataValidator, element);
validateChargeToIncomeAccountMappings(baseDataValidator, element); validateChargeToIncomeAccountMappings(baseDataValidator, element);
validateChargeOffToExpenseMappings(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)) { if (AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) {
@@ -1877,6 +1884,10 @@ public final class LoanProductDataValidator {
validatePaymentChannelFundSourceMappings(baseDataValidator, element); validatePaymentChannelFundSourceMappings(baseDataValidator, element);
validateChargeToIncomeAccountMappings(baseDataValidator, element); validateChargeToIncomeAccountMappings(baseDataValidator, element);
validateChargeOffToExpenseMappings(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); 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, public void validateMinMaxConstraints(final JsonElement element, final DataValidatorBuilder baseDataValidator,
final LoanProduct loanProduct) { final LoanProduct loanProduct) {
validatePrincipalMinMaxConstraint(element, loanProduct, baseDataValidator); validatePrincipalMinMaxConstraint(element, loanProduct, baseDataValidator);
@@ -216,4 +216,5 @@
<include file="parts/0195_create_loan_amortization_allocation_mapping.xml" relativeToChangelogFile="true" /> <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/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/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> </databaseChangeLog>
@@ -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>
@@ -73,7 +73,7 @@ class CreateJournalEntriesForChargeOffLoanTest {
Collections.emptyList(), false, "", null, null, null, null); Collections.emptyList(), false, "", null, null, null, null);
loanDTO = new LoanDTO(1L, 1L, 1L, "USD", false, true, true, List.of(loanTransactionDTO), false, false, chargeOffReasonId, false, loanDTO = new LoanDTO(1L, 1L, 1L, "USD", false, true, true, List.of(loanTransactionDTO), false, false, chargeOffReasonId, false,
false); false, null, null);
} }
@Test @Test
@@ -28,28 +28,38 @@ import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.BuyDownFeeAmortizationDetails; 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.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions; 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.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.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
import org.apache.fineract.client.models.PostLoansResponse; 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.client.util.CallFailedRuntimeException;
import org.apache.fineract.integrationtests.common.BusinessStepHelper; import org.apache.fineract.integrationtests.common.BusinessStepHelper;
import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.Utils; 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.BusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper; import org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
import org.apache.fineract.integrationtests.common.externalevents.LoanAdjustTransactionBusinessEvent; import org.apache.fineract.integrationtests.common.externalevents.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.LoanTransactionMinimalBusinessEvent; 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.AfterAll;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -88,7 +98,7 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
runAt("01 September 2024", () -> { runAt("01 September 2024", () -> {
clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
final PostLoanProductsResponse loanProductsResponse = loanProductHelper final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(createProgressiveLoanProductWithBuyDownFee()); .createLoanProduct(createProgressiveLoanProductWithBuyDownFee(null));
// Apply for the loan with proper progressive loan settings // Apply for the loan with proper progressive loan settings
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, 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 * 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 // Create a progressive loan product with accrual-based accounting and proper GL mappings
return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("BUY_DOWN_FEE_PROGRESSIVE_", 6)) PostLoanProductsRequest postLoanProductsRequest = new PostLoanProductsRequest()
.shortName(Utils.uniqueRandomStringGenerator("", 4)).description("Progressive loan product with buy down fee enabled") .name(Utils.uniqueRandomStringGenerator("BUY_DOWN_FEE_PROGRESSIVE_", 6)).shortName(Utils.uniqueRandomStringGenerator("", 4))
.includeInBorrowerCycle(false).useBorrowerCycle(false).currencyCode("USD").digitsAfterDecimal(2).principal(1000.0) .description("Progressive loan product with buy down fee enabled").includeInBorrowerCycle(false).useBorrowerCycle(false)
.minPrincipal(100.0).maxPrincipal(10000.0).numberOfRepayments(12).minNumberOfRepayments(6).maxNumberOfRepayments(24) .currencyCode("USD").digitsAfterDecimal(2).principal(1000.0).minPrincipal(100.0).maxPrincipal(10000.0)
.repaymentEvery(1).repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L).interestRatePerPeriod(10.0) .numberOfRepayments(12).minNumberOfRepayments(6).maxNumberOfRepayments(24).repaymentEvery(1)
.minInterestRatePerPeriod(0.0).maxInterestRatePerPeriod(120.0).interestRateFrequencyType(InterestRateFrequencyType.YEARS) .repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L).interestRatePerPeriod(10.0).minInterestRatePerPeriod(0.0)
.maxInterestRatePerPeriod(120.0).interestRateFrequencyType(InterestRateFrequencyType.YEARS)
.amortizationType(AmortizationType.EQUAL_INSTALLMENTS).interestType(InterestType.DECLINING_BALANCE) .amortizationType(AmortizationType.EQUAL_INSTALLMENTS).interestType(InterestType.DECLINING_BALANCE)
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).allowPartialPeriodInterestCalcualtion(false) .interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).allowPartialPeriodInterestCalcualtion(false)
.transactionProcessingStrategyCode("advanced-payment-allocation-strategy") .transactionProcessingStrategyCode("advanced-payment-allocation-strategy")
@@ -275,6 +287,11 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
.buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT) .buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
.buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION) .buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE).locale("en").dateFormat("dd MMMM yyyy"); .buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE).locale("en").dateFormat("dd MMMM yyyy");
if (buydownFeeClassificationAccountMappings != null) {
postLoanProductsRequest.addBuydownfeeClassificationToIncomeAccountMappingsItem(buydownFeeClassificationAccountMappings);
}
return postLoanProductsRequest;
} }
@Test @Test
@@ -485,6 +502,15 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
return response.getResourceId(); 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 @Test
public void testBuyDownFeeDailyAmortization() { public void testBuyDownFeeDailyAmortization() {
final AtomicReference<Long> loanIdRef = new AtomicReference<>(); final AtomicReference<Long> loanIdRef = new AtomicReference<>();
@@ -645,7 +671,7 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
@Test @Test
public void testRetrieveBuyDownFeeAmortizationDetails() { public void testRetrieveBuyDownFeeAmortizationDetails() {
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); 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, final long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 February 2024", 1000.0,
7.0, 6, null); 7.0, 6, null);
@@ -677,7 +703,7 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
public void testRetrieveBuyDownFeeAmortizationDetails_notEnabled() { public void testRetrieveBuyDownFeeAmortizationDetails_notEnabled() {
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProduct = loanProductHelper 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, final long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 February 2024", 1000.0,
7.0, 6, null); 7.0, 6, null);
@@ -872,7 +898,7 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
// Add initial buy down fee // Add initial buy down fee
final PostLoanProductsResponse loanProductsResponse = loanProductHelper final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(createProgressiveLoanProductWithBuyDownFee().merchantBuyDownFee(false)); .createLoanProduct(createProgressiveLoanProductWithBuyDownFee(null).merchantBuyDownFee(false));
// Apply for the loan with proper progressive loan settings // Apply for the loan with proper progressive loan settings
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, 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
});
}
} }
@@ -26,14 +26,17 @@ import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.apache.fineract.client.models.CapitalizedIncomeDetails; import org.apache.fineract.client.models.CapitalizedIncomeDetails;
import org.apache.fineract.client.models.GetCodesResponse; 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.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions; import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.LoanCapitalizedIncomeData; 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.PostClientsResponse;
import org.apache.fineract.client.models.PostCodeValueDataResponse; import org.apache.fineract.client.models.PostCodeValueDataResponse;
import org.apache.fineract.client.models.PostCodeValuesDataRequest; 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.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansResponse; 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.client.util.CallFailedRuntimeException;
import org.apache.fineract.integrationtests.common.BusinessStepHelper; import org.apache.fineract.integrationtests.common.BusinessStepHelper;
import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.Utils; 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.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.LoanBusinessEvent; import org.apache.fineract.integrationtests.common.externalevents.LoanBusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.LoanTransactionBusinessEvent; 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
});
}
} }