FINERACT-2113: Advanced Charge-off Expense Accounting - Add new "Advanced Accounting Rule"
This commit is contained in:
committed by
Adam Saghy
parent
75fa740ff6
commit
4ccb33a95e
+5
-2
@@ -63,10 +63,13 @@ public class ProductToGLAccountMapping extends AbstractPersistableCustom<Long> {
|
|||||||
@Column(name = "financial_account_type", nullable = true)
|
@Column(name = "financial_account_type", nullable = true)
|
||||||
private int financialAccountType;
|
private int financialAccountType;
|
||||||
|
|
||||||
|
@Column(name = "charge_off_reason_id", nullable = true)
|
||||||
|
private Long chargeOffReasonId;
|
||||||
|
|
||||||
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 int financialAccountType, final Long chargeOffReasonId) {
|
||||||
|
|
||||||
return new ProductToGLAccountMapping().setGlAccount(glAccount).setProductId(productId).setProductType(productType)
|
return new ProductToGLAccountMapping().setGlAccount(glAccount).setProductId(productId).setProductType(productType)
|
||||||
.setFinancialAccountType(financialAccountType);
|
.setFinancialAccountType(financialAccountType).setChargeOffReasonId(chargeOffReasonId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -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")
|
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReasonId is NULL")
|
||||||
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);
|
||||||
|
|
||||||
@@ -61,4 +61,8 @@ public interface ProductToGLAccountMappingRepository
|
|||||||
@Param("productType") int productType);
|
@Param("productType") int productType);
|
||||||
|
|
||||||
List<ProductToGLAccountMapping> findByProductIdAndProductType(Long productId, int productType);
|
List<ProductToGLAccountMapping> findByProductIdAndProductType(Long productId, int productType);
|
||||||
|
|
||||||
|
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.chargeOffReasonId is not NULL")
|
||||||
|
List<ProductToGLAccountMapping> findAllChargesOffReasonsMappings(@Param("productId") Long productId,
|
||||||
|
@Param("productType") int productType);
|
||||||
}
|
}
|
||||||
|
|||||||
+139
@@ -27,6 +27,7 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan;
|
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan;
|
||||||
@@ -39,7 +40,11 @@ import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGL
|
|||||||
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
|
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
|
||||||
import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingInvalidException;
|
import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingInvalidException;
|
||||||
import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingNotFoundException;
|
import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingNotFoundException;
|
||||||
|
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||||
|
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||||
@@ -53,6 +58,7 @@ import org.springframework.stereotype.Component;
|
|||||||
public class ProductToGLAccountMappingHelper {
|
public class ProductToGLAccountMappingHelper {
|
||||||
|
|
||||||
protected static final List<GLAccountType> ASSET_LIABILITY_TYPES = List.of(GLAccountType.ASSET, GLAccountType.LIABILITY);
|
protected static final List<GLAccountType> ASSET_LIABILITY_TYPES = List.of(GLAccountType.ASSET, GLAccountType.LIABILITY);
|
||||||
|
private static final Integer GL_ACCOUNT_EXPENSE_TYPE = 5;
|
||||||
|
|
||||||
protected final GLAccountRepository accountRepository;
|
protected final GLAccountRepository accountRepository;
|
||||||
protected final ProductToGLAccountMappingRepository accountMappingRepository;
|
protected final ProductToGLAccountMappingRepository accountMappingRepository;
|
||||||
@@ -60,6 +66,7 @@ public class ProductToGLAccountMappingHelper {
|
|||||||
private final ChargeRepositoryWrapper chargeRepositoryWrapper;
|
private final ChargeRepositoryWrapper chargeRepositoryWrapper;
|
||||||
protected final GLAccountRepositoryWrapper accountRepositoryWrapper;
|
protected final GLAccountRepositoryWrapper accountRepositoryWrapper;
|
||||||
private final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper;
|
private final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper;
|
||||||
|
private final CodeValueRepository codeValueRepository;
|
||||||
|
|
||||||
public void saveProductToAccountMapping(final JsonElement element, final String paramName, final Long productId,
|
public void saveProductToAccountMapping(final JsonElement element, final String paramName, final Long productId,
|
||||||
final int placeHolderTypeId, final GLAccountType expectedAccountType, final PortfolioProductType portfolioProductType) {
|
final int placeHolderTypeId, final GLAccountType expectedAccountType, final PortfolioProductType portfolioProductType) {
|
||||||
@@ -194,6 +201,27 @@ public class ProductToGLAccountMappingHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void saveChargeOffReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
|
||||||
|
final Map<String, Object> changes, final PortfolioProductType portfolioProductType) {
|
||||||
|
|
||||||
|
final String arrayName = LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue();
|
||||||
|
final JsonArray chargeOffReasonToExpenseAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayName, element);
|
||||||
|
|
||||||
|
if (chargeOffReasonToExpenseAccountMappingArray != null) {
|
||||||
|
if (changes != null) {
|
||||||
|
changes.put(arrayName, command.jsonFragment(arrayName));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < chargeOffReasonToExpenseAccountMappingArray.size(); i++) {
|
||||||
|
final JsonObject jsonObject = chargeOffReasonToExpenseAccountMappingArray.get(i).getAsJsonObject();
|
||||||
|
final Long reasonId = jsonObject.get(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue()).getAsLong();
|
||||||
|
final Long expenseAccountId = jsonObject.get(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()).getAsLong();
|
||||||
|
|
||||||
|
saveChargeOffReasonToExpenseMapping(productId, reasonId, expenseAccountId, portfolioProductType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param command
|
* @param command
|
||||||
* @param element
|
* @param element
|
||||||
@@ -356,6 +384,65 @@ public class ProductToGLAccountMappingHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateChargeOffReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
|
||||||
|
final Map<String, Object> changes, final PortfolioProductType portfolioProductType) {
|
||||||
|
|
||||||
|
final List<ProductToGLAccountMapping> existingChargeOffReasonToGLAccountMappings = this.accountMappingRepository
|
||||||
|
.findAllChargesOffReasonsMappings(productId, portfolioProductType.getValue());
|
||||||
|
final JsonArray chargeOffReasonToGLAccountMappingArray = this.fromApiJsonHelper
|
||||||
|
.extractJsonArrayNamed(LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(), element);
|
||||||
|
|
||||||
|
final Map<Long, Long> inputChargeOffReasonToGLAccountMap = new HashMap<>();
|
||||||
|
|
||||||
|
final Set<Long> existingChargeOffReasons = new HashSet<>();
|
||||||
|
if (chargeOffReasonToGLAccountMappingArray != null) {
|
||||||
|
if (changes != null) {
|
||||||
|
changes.put(LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(),
|
||||||
|
command.jsonFragment(LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < chargeOffReasonToGLAccountMappingArray.size(); i++) {
|
||||||
|
final JsonObject jsonObject = chargeOffReasonToGLAccountMappingArray.get(i).getAsJsonObject();
|
||||||
|
final Long expenseGlAccountId = jsonObject.get(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()).getAsLong();
|
||||||
|
final Long chargeOffReasonCodeValueId = jsonObject
|
||||||
|
.get(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue()).getAsLong();
|
||||||
|
inputChargeOffReasonToGLAccountMap.put(chargeOffReasonCodeValueId, expenseGlAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If input map is empty, delete all existing mappings
|
||||||
|
if (inputChargeOffReasonToGLAccountMap.isEmpty()) {
|
||||||
|
this.accountMappingRepository.deleteAllInBatch(existingChargeOffReasonToGLAccountMappings);
|
||||||
|
} else {
|
||||||
|
for (final ProductToGLAccountMapping existingChargeOffReasonToGLAccountMapping : existingChargeOffReasonToGLAccountMappings) {
|
||||||
|
final Long currentChargeOffReasonId = existingChargeOffReasonToGLAccountMapping.getChargeOffReasonId();
|
||||||
|
if (currentChargeOffReasonId != null) {
|
||||||
|
existingChargeOffReasons.add(currentChargeOffReasonId);
|
||||||
|
// update existing mappings (if required)
|
||||||
|
if (inputChargeOffReasonToGLAccountMap.containsKey(currentChargeOffReasonId)) {
|
||||||
|
final Long newGLAccountId = inputChargeOffReasonToGLAccountMap.get(currentChargeOffReasonId);
|
||||||
|
if (!newGLAccountId.equals(existingChargeOffReasonToGLAccountMapping.getGlAccount().getId())) {
|
||||||
|
final Optional<GLAccount> glAccount = accountRepository.findById(newGLAccountId);
|
||||||
|
if (glAccount.isPresent()) {
|
||||||
|
existingChargeOffReasonToGLAccountMapping.setGlAccount(glAccount.get());
|
||||||
|
this.accountMappingRepository.saveAndFlush(existingChargeOffReasonToGLAccountMapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // deleted payment type
|
||||||
|
else {
|
||||||
|
this.accountMappingRepository.delete(existingChargeOffReasonToGLAccountMapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only the newly added
|
||||||
|
for (Map.Entry<Long, Long> entry : inputChargeOffReasonToGLAccountMap.entrySet().stream()
|
||||||
|
.filter(e -> !existingChargeOffReasons.contains(e.getKey())).toList()) {
|
||||||
|
saveChargeOffReasonToExpenseMapping(productId, entry.getKey(), entry.getValue(), portfolioProductType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param productId
|
* @param productId
|
||||||
*
|
*
|
||||||
@@ -402,6 +489,24 @@ public class ProductToGLAccountMappingHelper {
|
|||||||
this.accountMappingRepository.saveAndFlush(accountMapping);
|
this.accountMappingRepository.saveAndFlush(accountMapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void saveChargeOffReasonToExpenseMapping(final Long productId, final Long reasonId, final Long expenseAccountId,
|
||||||
|
final PortfolioProductType portfolioProductType) {
|
||||||
|
|
||||||
|
final Optional<GLAccount> glAccount = accountRepository.findById(expenseAccountId);
|
||||||
|
|
||||||
|
final boolean reasonMappingExists = this.accountMappingRepository
|
||||||
|
.findAllChargesOffReasonsMappings(productId, portfolioProductType.getValue()).stream()
|
||||||
|
.anyMatch(mapping -> mapping.getChargeOffReasonId().equals(reasonId));
|
||||||
|
|
||||||
|
if (glAccount.isPresent() && !reasonMappingExists) {
|
||||||
|
final ProductToGLAccountMapping accountMapping = new ProductToGLAccountMapping().setGlAccount(glAccount.get())
|
||||||
|
.setProductId(productId).setProductType(portfolioProductType.getValue())
|
||||||
|
.setFinancialAccountType(CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue()).setChargeOffReasonId(reasonId);
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -455,4 +560,38 @@ public class ProductToGLAccountMappingHelper {
|
|||||||
this.accountMappingRepository.deleteAllInBatch(productToGLAccountMappings);
|
this.accountMappingRepository.deleteAllInBatch(productToGLAccountMappings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void validateChargeOffMappingsInDatabase(final List<JsonObject> mappings) {
|
||||||
|
final List<ApiParameterError> validationErrors = new ArrayList<>();
|
||||||
|
|
||||||
|
for (JsonObject jsonObject : mappings) {
|
||||||
|
final Long expenseGlAccountId = this.fromApiJsonHelper
|
||||||
|
.extractLongNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject);
|
||||||
|
final Long chargeOffReasonCodeValueId = this.fromApiJsonHelper
|
||||||
|
.extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), jsonObject);
|
||||||
|
|
||||||
|
// Validation: chargeOffReasonCodeValueId must exist in the database
|
||||||
|
CodeValue codeValue = this.codeValueRepository.findByCodeNameAndId("ChargeOffReasons", chargeOffReasonCodeValueId);
|
||||||
|
if (codeValue == null) {
|
||||||
|
validationErrors.add(ApiParameterError.parameterError("validation.msg.chargeoffreason.invalid",
|
||||||
|
"Charge-off reason with ID " + chargeOffReasonCodeValueId + " does not exist",
|
||||||
|
LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation: expenseGLAccountId must exist as a valid Expense GL account
|
||||||
|
final Optional<GLAccount> glAccount = accountRepository.findById(expenseGlAccountId);
|
||||||
|
|
||||||
|
if (glAccount.isEmpty() || !glAccount.get().getType().equals(GL_ACCOUNT_EXPENSE_TYPE)) {
|
||||||
|
validationErrors.add(ApiParameterError.parameterError("validation.msg.glaccount.not.found",
|
||||||
|
"GL Account with ID " + expenseGlAccountId + " does not exist or is not an Expense GL account",
|
||||||
|
LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw all collected validation errors, if any
|
||||||
|
if (!validationErrors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException(validationErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -46,5 +46,4 @@ public interface ProductToGLAccountMappingReadPlatformService {
|
|||||||
List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForShareProduct(Long productId);
|
List<PaymentTypeToGLAccountMapper> fetchPaymentTypeToFundSourceMappingsForShareProduct(Long productId);
|
||||||
|
|
||||||
List<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForShareProduct(Long productId);
|
List<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForShareProduct(Long productId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -29,6 +29,7 @@ import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository;
|
|||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||||
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
|
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
|
||||||
|
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||||
@@ -42,9 +43,9 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
|
|||||||
public SavingsProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository,
|
public SavingsProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository,
|
||||||
final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper,
|
final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper,
|
||||||
final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper,
|
final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper,
|
||||||
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper) {
|
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper, final CodeValueRepository codeValueRepository) {
|
||||||
super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper,
|
super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper,
|
||||||
paymentTypeRepositoryWrapper);
|
paymentTypeRepositoryWrapper, codeValueRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
/***
|
||||||
|
|||||||
+3
-2
@@ -28,6 +28,7 @@ import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository;
|
|||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepositoryWrapper;
|
||||||
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
||||||
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
|
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
|
||||||
|
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||||
@@ -41,9 +42,9 @@ public class ShareProductToGLAccountMappingHelper extends ProductToGLAccountMapp
|
|||||||
public ShareProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository,
|
public ShareProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository,
|
||||||
final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper,
|
final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper,
|
||||||
final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper,
|
final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper,
|
||||||
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper) {
|
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper, final CodeValueRepository codeValueRepository) {
|
||||||
super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper,
|
super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper,
|
||||||
paymentTypeRepositoryWrapper);
|
paymentTypeRepositoryWrapper, codeValueRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
/***
|
||||||
|
|||||||
+4
-1
@@ -172,7 +172,10 @@ public final class AccountingConstants {
|
|||||||
INCOME_FROM_CHARGE_OFF_PENALTY("incomeFromChargeOffPenaltyAccountId"), //
|
INCOME_FROM_CHARGE_OFF_PENALTY("incomeFromChargeOffPenaltyAccountId"), //
|
||||||
INCOME_FROM_GOODWILL_CREDIT_INTEREST("incomeFromGoodwillCreditInterestAccountId"), //
|
INCOME_FROM_GOODWILL_CREDIT_INTEREST("incomeFromGoodwillCreditInterestAccountId"), //
|
||||||
INCOME_FROM_GOODWILL_CREDIT_FEES("incomeFromGoodwillCreditFeesAccountId"), //
|
INCOME_FROM_GOODWILL_CREDIT_FEES("incomeFromGoodwillCreditFeesAccountId"), //
|
||||||
INCOME_FROM_GOODWILL_CREDIT_PENALTY("incomeFromGoodwillCreditPenaltyAccountId"); //
|
INCOME_FROM_GOODWILL_CREDIT_PENALTY("incomeFromGoodwillCreditPenaltyAccountId"), //
|
||||||
|
CHARGE_OFF_REASONS_TO_EXPENSE("chargeOffReasonsToExpenseMappings"), //
|
||||||
|
EXPENSE_GL_ACCOUNT_ID("expenseGLAccountId"), //
|
||||||
|
CHARGE_OFF_REASON_CODE_VALUE_ID("chargeOffReasonCodeValueId"); //
|
||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
|
|||||||
+13
-3
@@ -33,6 +33,7 @@ import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
|
|||||||
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
|
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
|
||||||
import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingInvalidException;
|
import org.apache.fineract.accounting.producttoaccountmapping.exception.ProductToGLAccountMappingInvalidException;
|
||||||
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingHelper;
|
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingHelper;
|
||||||
|
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||||
@@ -46,9 +47,9 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
|
|||||||
public LoanProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository,
|
public LoanProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository,
|
||||||
final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper,
|
final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper,
|
||||||
final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper,
|
final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper,
|
||||||
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper) {
|
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper, final CodeValueRepository codeValueRepository) {
|
||||||
super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper,
|
super(glAccountRepository, glAccountMappingRepository, fromApiJsonHelper, chargeRepositoryWrapper, accountRepositoryWrapper,
|
||||||
paymentTypeRepositoryWrapper);
|
paymentTypeRepositoryWrapper, codeValueRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
/***
|
||||||
@@ -138,6 +139,16 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
|
|||||||
saveChargesToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, false);
|
saveChargesToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void saveChargeOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
|
||||||
|
final Map<String, Object> changes) {
|
||||||
|
saveChargeOffReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateChargeOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
|
||||||
|
final Map<String, Object> changes) {
|
||||||
|
updateChargeOffReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN);
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -387,5 +398,4 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
|
|||||||
}
|
}
|
||||||
return gLAccountType;
|
return gLAccountType;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -246,6 +246,7 @@ final class LoanProductsApiResourceSwagger {
|
|||||||
public Long incomeFromGoodwillCreditPenaltyAccountId;
|
public Long incomeFromGoodwillCreditPenaltyAccountId;
|
||||||
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
||||||
public List<GetLoanProductsProductIdResponse.GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
|
public List<GetLoanProductsProductIdResponse.GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
|
||||||
|
public List<GetLoanProductsProductIdResponse.GetChargeOffReasonsToExpenseMappings> chargeOffReasonsToExpenseMappings;
|
||||||
public List<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
public List<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
||||||
|
|
||||||
// Multi Disburse
|
// Multi Disburse
|
||||||
@@ -1209,6 +1210,16 @@ final class LoanProductsApiResourceSwagger {
|
|||||||
public Long fundSourceAccountId;
|
public Long fundSourceAccountId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static final class GetChargeOffReasonsToExpenseMappings {
|
||||||
|
|
||||||
|
private GetChargeOffReasonsToExpenseMappings() {}
|
||||||
|
|
||||||
|
@Schema(example = "1")
|
||||||
|
public Long chargeOffReasonCodeValueId;
|
||||||
|
@Schema(example = "12")
|
||||||
|
public Long expenseGLAccountId;
|
||||||
|
}
|
||||||
|
|
||||||
static final class GetLoanFeeToIncomeAccountMappings {
|
static final class GetLoanFeeToIncomeAccountMappings {
|
||||||
|
|
||||||
private GetLoanFeeToIncomeAccountMappings() {}
|
private GetLoanFeeToIncomeAccountMappings() {}
|
||||||
@@ -1319,6 +1330,7 @@ final class LoanProductsApiResourceSwagger {
|
|||||||
public GetLoanAccountingMappings accountingMappings;
|
public GetLoanAccountingMappings accountingMappings;
|
||||||
public Set<GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
public Set<GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
||||||
public Set<GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
|
public Set<GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
|
||||||
|
public Set<GetChargeOffReasonsToExpenseMappings> chargeOffReasonsToExpenseMappings;
|
||||||
@Schema(example = "false")
|
@Schema(example = "false")
|
||||||
public Boolean isRatesEnabled;
|
public Boolean isRatesEnabled;
|
||||||
@Schema(example = "true")
|
@Schema(example = "true")
|
||||||
@@ -1577,6 +1589,7 @@ final class LoanProductsApiResourceSwagger {
|
|||||||
public Long incomeFromChargeOffPenaltyAccountId;
|
public Long incomeFromChargeOffPenaltyAccountId;
|
||||||
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
||||||
public List<GetLoanProductsProductIdResponse.GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
|
public List<GetLoanProductsProductIdResponse.GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
|
||||||
|
public List<GetLoanProductsProductIdResponse.GetChargeOffReasonsToExpenseMappings> chargeOffReasonsToExpenseMappings;
|
||||||
public List<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
public List<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
||||||
@Schema(example = "false")
|
@Schema(example = "false")
|
||||||
public Boolean enableAccrualActivityPosting;
|
public Boolean enableAccrualActivityPosting;
|
||||||
|
|||||||
+3
@@ -151,6 +151,7 @@ public class LoanProductData implements Serializable {
|
|||||||
private Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
|
private Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
|
||||||
private Collection<ChargeToGLAccountMapper> feeToIncomeAccountMappings;
|
private Collection<ChargeToGLAccountMapper> feeToIncomeAccountMappings;
|
||||||
private Collection<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
private Collection<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
||||||
|
private List<ChargeToGLAccountMapper> chargeOffReasonsToExpenseMappings;
|
||||||
private final boolean enableAccrualActivityPosting;
|
private final boolean enableAccrualActivityPosting;
|
||||||
|
|
||||||
// rates
|
// rates
|
||||||
@@ -853,6 +854,7 @@ public class LoanProductData implements Serializable {
|
|||||||
this.paymentChannelToFundSourceMappings = null;
|
this.paymentChannelToFundSourceMappings = null;
|
||||||
this.feeToIncomeAccountMappings = null;
|
this.feeToIncomeAccountMappings = null;
|
||||||
this.penaltyToIncomeAccountMappings = null;
|
this.penaltyToIncomeAccountMappings = null;
|
||||||
|
this.chargeOffReasonsToExpenseMappings = null;
|
||||||
this.valueConditionTypeOptions = null;
|
this.valueConditionTypeOptions = null;
|
||||||
this.principalVariationsForBorrowerCycle = principalVariations;
|
this.principalVariationsForBorrowerCycle = principalVariations;
|
||||||
this.interestRateVariationsForBorrowerCycle = interestRateVariations;
|
this.interestRateVariationsForBorrowerCycle = interestRateVariations;
|
||||||
@@ -992,6 +994,7 @@ public class LoanProductData implements Serializable {
|
|||||||
this.paymentChannelToFundSourceMappings = productData.paymentChannelToFundSourceMappings;
|
this.paymentChannelToFundSourceMappings = productData.paymentChannelToFundSourceMappings;
|
||||||
this.feeToIncomeAccountMappings = productData.feeToIncomeAccountMappings;
|
this.feeToIncomeAccountMappings = productData.feeToIncomeAccountMappings;
|
||||||
this.penaltyToIncomeAccountMappings = productData.penaltyToIncomeAccountMappings;
|
this.penaltyToIncomeAccountMappings = productData.penaltyToIncomeAccountMappings;
|
||||||
|
this.chargeOffReasonsToExpenseMappings = productData.chargeOffReasonsToExpenseMappings;
|
||||||
|
|
||||||
this.chargeOptions = chargeOptions;
|
this.chargeOptions = chargeOptions;
|
||||||
this.penaltyOptions = penaltyOptions;
|
this.penaltyOptions = penaltyOptions;
|
||||||
|
|||||||
+4
@@ -130,6 +130,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
|||||||
// advanced accounting mappings
|
// advanced accounting mappings
|
||||||
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);
|
||||||
break;
|
break;
|
||||||
case ACCRUAL_UPFRONT:
|
case ACCRUAL_UPFRONT:
|
||||||
// Fall Through
|
// Fall Through
|
||||||
@@ -208,6 +209,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
|||||||
// advanced accounting mappings
|
// advanced accounting mappings
|
||||||
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);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,6 +381,8 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
|||||||
accountingRuleType);
|
accountingRuleType);
|
||||||
this.loanProductToGLAccountMappingHelper.updatePaymentChannelToFundSourceMappings(command, element, loanProductId, changes);
|
this.loanProductToGLAccountMappingHelper.updatePaymentChannelToFundSourceMappings(command, element, loanProductId, changes);
|
||||||
this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes);
|
this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes);
|
||||||
|
this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId,
|
||||||
|
changes);
|
||||||
}
|
}
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -160,7 +160,7 @@ public class LoanProductsApiResource {
|
|||||||
@Operation(summary = "Create a Loan Product", description = "Depending of the Accounting Rule (accountingRule) selected, additional fields with details of the appropriate Ledger Account identifiers would need to be passed in.\n"
|
@Operation(summary = "Create a Loan Product", description = "Depending of the Accounting Rule (accountingRule) selected, additional fields with details of the appropriate Ledger Account identifiers would need to be passed in.\n"
|
||||||
+ "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n"
|
+ "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n"
|
||||||
+ "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n"
|
+ "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n"
|
||||||
+ "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n"
|
+ "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, chargeOffReasonsToExpenseMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n"
|
||||||
+ "Additional Mandatory Fields for Cash(2) based accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
|
+ "Additional Mandatory Fields for Cash(2) based accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
|
||||||
+ "Additional Mandatory Fields for periodic (3) and upfront (4)accrual accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
|
+ "Additional Mandatory Fields for periodic (3) and upfront (4)accrual accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
|
||||||
+ "Additional Mandatory Fields if interest recalculation is enabled(true): interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType\n\n"
|
+ "Additional Mandatory Fields if interest recalculation is enabled(true): interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType\n\n"
|
||||||
@@ -336,6 +336,7 @@ public class LoanProductsApiResource {
|
|||||||
Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
|
Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
|
||||||
Collection<ChargeToGLAccountMapper> feeToGLAccountMappings;
|
Collection<ChargeToGLAccountMapper> feeToGLAccountMappings;
|
||||||
Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings;
|
Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings;
|
||||||
|
List<ChargeToGLAccountMapper> chargeOffReasonsToExpenseMappings;
|
||||||
if (loanProduct.hasAccountingEnabled()) {
|
if (loanProduct.hasAccountingEnabled()) {
|
||||||
accountingMappings = this.accountMappingReadPlatformService.fetchAccountMappingDetailsForLoanProduct(productId,
|
accountingMappings = this.accountMappingReadPlatformService.fetchAccountMappingDetailsForLoanProduct(productId,
|
||||||
loanProduct.getAccountingRule().getId().intValue());
|
loanProduct.getAccountingRule().getId().intValue());
|
||||||
|
|||||||
+62
-1
@@ -27,6 +27,7 @@ import java.math.BigDecimal;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -36,6 +37,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
|
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
|
||||||
import org.apache.fineract.accounting.common.AccountingValidations;
|
import org.apache.fineract.accounting.common.AccountingValidations;
|
||||||
|
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingHelper;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
@@ -141,7 +143,9 @@ public final class LoanProductDataValidator {
|
|||||||
LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(),
|
LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(),
|
||||||
LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(),
|
LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(),
|
||||||
LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(),
|
LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(),
|
||||||
LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME,
|
LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(),
|
||||||
|
LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(),
|
||||||
|
LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME,
|
||||||
LoanProductConstants.PRINCIPAL_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME,
|
LoanProductConstants.PRINCIPAL_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME,
|
||||||
LoanProductConstants.INTEREST_RATE_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME,
|
LoanProductConstants.INTEREST_RATE_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME,
|
||||||
LoanProductConstants.NUMBER_OF_REPAYMENT_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.SHORT_NAME,
|
LoanProductConstants.NUMBER_OF_REPAYMENT_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.SHORT_NAME,
|
||||||
@@ -195,6 +199,7 @@ public final class LoanProductDataValidator {
|
|||||||
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
|
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
|
||||||
private final AdvancedPaymentAllocationsJsonParser advancedPaymentAllocationsJsonParser;
|
private final AdvancedPaymentAllocationsJsonParser advancedPaymentAllocationsJsonParser;
|
||||||
private final AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator;
|
private final AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator;
|
||||||
|
private final ProductToGLAccountMappingHelper productToGLAccountMappingHelper;
|
||||||
|
|
||||||
public void validateForCreate(final JsonCommand command) {
|
public void validateForCreate(final JsonCommand command) {
|
||||||
String json = command.json();
|
String json = command.json();
|
||||||
@@ -718,6 +723,7 @@ public final class LoanProductDataValidator {
|
|||||||
|
|
||||||
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
|
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
|
||||||
validateChargeToIncomeAccountMappings(baseDataValidator, element);
|
validateChargeToIncomeAccountMappings(baseDataValidator, element);
|
||||||
|
validateChargeOffToExpenseMappings(baseDataValidator, element);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1791,6 +1797,7 @@ public final class LoanProductDataValidator {
|
|||||||
|
|
||||||
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
|
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
|
||||||
validateChargeToIncomeAccountMappings(baseDataValidator, element);
|
validateChargeToIncomeAccountMappings(baseDataValidator, element);
|
||||||
|
validateChargeOffToExpenseMappings(baseDataValidator, element);
|
||||||
|
|
||||||
validateMinMaxConstraints(element, baseDataValidator, loanProduct);
|
validateMinMaxConstraints(element, baseDataValidator, loanProduct);
|
||||||
|
|
||||||
@@ -1964,6 +1971,60 @@ public final class LoanProductDataValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateChargeOffToExpenseMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
||||||
|
String parameterName = LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue();
|
||||||
|
|
||||||
|
if (this.fromApiJsonHelper.parameterExists(parameterName, element)) {
|
||||||
|
final JsonArray chargeOffToExpenseMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element);
|
||||||
|
if (chargeOffToExpenseMappingArray != null && chargeOffToExpenseMappingArray.size() > 0) {
|
||||||
|
Map<Long, Set<Long>> chargeOffReasonToAccounts = new HashMap<>();
|
||||||
|
List<JsonObject> processedMappings = new ArrayList<>(); // Collect processed mappings for the new method
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
do {
|
||||||
|
final JsonObject jsonObject = chargeOffToExpenseMappingArray.get(i).getAsJsonObject();
|
||||||
|
final Long expenseGlAccountId = this.fromApiJsonHelper
|
||||||
|
.extractLongNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject);
|
||||||
|
final Long chargeOffReasonCodeValueId = this.fromApiJsonHelper
|
||||||
|
.extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), jsonObject);
|
||||||
|
|
||||||
|
// Validate parameters locally
|
||||||
|
baseDataValidator.reset()
|
||||||
|
.parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT
|
||||||
|
+ LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue())
|
||||||
|
.value(expenseGlAccountId).notNull().integerGreaterThanZero();
|
||||||
|
baseDataValidator.reset()
|
||||||
|
.parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT
|
||||||
|
+ LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue())
|
||||||
|
.value(chargeOffReasonCodeValueId).notNull().integerGreaterThanZero();
|
||||||
|
|
||||||
|
// Handle duplicate charge-off reason and GL Account validation
|
||||||
|
chargeOffReasonToAccounts.putIfAbsent(chargeOffReasonCodeValueId, new HashSet<>());
|
||||||
|
Set<Long> associatedAccounts = chargeOffReasonToAccounts.get(chargeOffReasonCodeValueId);
|
||||||
|
|
||||||
|
if (associatedAccounts.contains(expenseGlAccountId)) {
|
||||||
|
baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET)
|
||||||
|
.failWithCode("duplicate.chargeOffReason.and.glAccount");
|
||||||
|
}
|
||||||
|
associatedAccounts.add(expenseGlAccountId);
|
||||||
|
|
||||||
|
if (associatedAccounts.size() > 1) {
|
||||||
|
baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET)
|
||||||
|
.failWithCode("multiple.glAccounts.for.chargeOffReason");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect mapping for additional validations
|
||||||
|
processedMappings.add(jsonObject);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
} while (i < chargeOffToExpenseMappingArray.size());
|
||||||
|
|
||||||
|
// Call the new validation method for additional checks
|
||||||
|
productToGLAccountMappingHelper.validateChargeOffMappingsInDatabase(processedMappings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -171,4 +171,5 @@
|
|||||||
<include file="parts/0150_transaction_summary_with_asset_owner_report_interest_waiver_interest_refund_added.xml" relativeToChangelogFile="true" />
|
<include file="parts/0150_transaction_summary_with_asset_owner_report_interest_waiver_interest_refund_added.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0151_interest_refund_business_events.xml" relativeToChangelogFile="true" />
|
<include file="parts/0151_interest_refund_business_events.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0152_update_password_validation_policy.xml" relativeToChangelogFile="true" />
|
<include file="parts/0152_update_password_validation_policy.xml" relativeToChangelogFile="true" />
|
||||||
|
<include file="parts/0153_add_charge_off_reason_id_to_acc_product_mapping.xml" relativeToChangelogFile="true" />
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
<?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="charge_off_reason_id" type="INT" defaultValueNumeric="NULL">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
<addForeignKeyConstraint
|
||||||
|
baseTableName="acc_product_mapping"
|
||||||
|
baseColumnNames="charge_off_reason_id"
|
||||||
|
referencedTableName="m_code_value"
|
||||||
|
referencedColumnNames="id"
|
||||||
|
constraintName="fk_acc_product_mapping_charge_off_reason"/>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+272
@@ -0,0 +1,272 @@
|
|||||||
|
/**
|
||||||
|
* 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.integrationtests;
|
||||||
|
|
||||||
|
import static org.apache.fineract.integrationtests.common.funds.FundsResourceHandler.createFund;
|
||||||
|
|
||||||
|
import io.restassured.builder.RequestSpecBuilder;
|
||||||
|
import io.restassured.builder.ResponseSpecBuilder;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import io.restassured.specification.RequestSpecification;
|
||||||
|
import io.restassured.specification.ResponseSpecification;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.client.models.AllowAttributeOverrides;
|
||||||
|
import org.apache.fineract.client.models.ChargeData;
|
||||||
|
import org.apache.fineract.client.models.ChargeToGLAccountMapper;
|
||||||
|
import org.apache.fineract.client.models.GetChargeOffReasonsToExpenseMappings;
|
||||||
|
import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings;
|
||||||
|
import org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings;
|
||||||
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
|
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
|
||||||
|
import org.apache.fineract.client.util.CallFailedRuntimeException;
|
||||||
|
import org.apache.fineract.integrationtests.common.BusinessStepHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.Utils;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.system.CodeHelper;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
|
@ExtendWith(LoanTestLifecycleExtension.class)
|
||||||
|
public class LoanProductChargeOffReasonMappingsTest extends BaseLoanIntegrationTest {
|
||||||
|
|
||||||
|
private static final String CODE_VALUE_NAME = "ChargeOffReasons";
|
||||||
|
|
||||||
|
private static ResponseSpecification responseSpec;
|
||||||
|
private static RequestSpecification requestSpec;
|
||||||
|
private static LoanTransactionHelper loanTransactionHelper;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void setup() {
|
||||||
|
Utils.initializeRESTAssured();
|
||||||
|
requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
|
||||||
|
requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
|
||||||
|
requestSpec.header("Fineract-Platform-TenantId", "default");
|
||||||
|
responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
|
||||||
|
loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
|
||||||
|
BusinessStepHelper businessStepHelper = new BusinessStepHelper();
|
||||||
|
// setup COB Business Steps to prevent test failing due other integration test configurations
|
||||||
|
businessStepHelper.updateSteps("LOAN_CLOSE_OF_BUSINESS", "APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION",
|
||||||
|
"CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
|
||||||
|
"EXTERNAL_ASSET_OWNER_TRANSFER", "ACCRUAL_ACTIVITY_POSTING");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateLoanProductWithValidChargeOffReason() {
|
||||||
|
final String creationBusinessDay = "15 January 2023";
|
||||||
|
runAt(creationBusinessDay, () -> {
|
||||||
|
Integer chargeOffReasons = createChargeOffReason();
|
||||||
|
Long localLoanProductId = loanTransactionHelper.createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), 15L))
|
||||||
|
.getResourceId();
|
||||||
|
|
||||||
|
Assertions.assertNotNull(localLoanProductId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateLoanProductWithValidChargeOffReason() {
|
||||||
|
final String creationBusinessDay = "15 January 2023";
|
||||||
|
runAt(creationBusinessDay, () -> {
|
||||||
|
Integer chargeOffReasons = createChargeOffReason();
|
||||||
|
List<GetChargeOffReasonsToExpenseMappings> chargeOffReasonsToExpenseMappings = new ArrayList<>();
|
||||||
|
GetChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new GetChargeOffReasonsToExpenseMappings();
|
||||||
|
getChargeOffReasonsToExpenseMappings.setChargeOffReasonCodeValueId(Long.valueOf(chargeOffReasons));
|
||||||
|
getChargeOffReasonsToExpenseMappings.setExpenseGLAccountId(15L);
|
||||||
|
chargeOffReasonsToExpenseMappings.add(getChargeOffReasonsToExpenseMappings);
|
||||||
|
|
||||||
|
Long localLoanProductId = loanTransactionHelper.updateLoanProduct(1L,
|
||||||
|
new PutLoanProductsProductIdRequest().locale("en").chargeOffReasonsToExpenseMappings(chargeOffReasonsToExpenseMappings))
|
||||||
|
.getResourceId();
|
||||||
|
|
||||||
|
Assertions.assertNotNull(localLoanProductId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateLoanProductWithInvalidGLAccount() {
|
||||||
|
final String creationBusinessDay = "15 January 2023";
|
||||||
|
runAt(creationBusinessDay, () -> {
|
||||||
|
try {
|
||||||
|
Integer chargeOffReasons = createChargeOffReason();
|
||||||
|
loanTransactionHelper.createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), 9999L));
|
||||||
|
} catch (CallFailedRuntimeException e) {
|
||||||
|
Assertions.assertTrue(e.getMessage().contains("validation.msg.glaccount.not.found"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateLoanProductWithInvalidChargeOffReason() {
|
||||||
|
final String creationBusinessDay = "15 January 2023";
|
||||||
|
runAt(creationBusinessDay, () -> {
|
||||||
|
try {
|
||||||
|
loanTransactionHelper.createLoanProduct(loanProductsRequest(1L, 12L));
|
||||||
|
} catch (CallFailedRuntimeException e) {
|
||||||
|
Assertions.assertTrue(e.getMessage().contains("validation.msg.chargeoffreason.invalid"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private PostLoanProductsRequest loanProductsRequest(Long chargeOffReasonId, Long glAccountId) {
|
||||||
|
String name = Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6);
|
||||||
|
String shortName = Utils.uniqueRandomStringGenerator("", 4);
|
||||||
|
|
||||||
|
List<Integer> principalVariationsForBorrowerCycle = new ArrayList<>();
|
||||||
|
List<Integer> numberOfRepaymentVariationsForBorrowerCycle = new ArrayList<>();
|
||||||
|
List<Integer> interestRateVariationsForBorrowerCycle = new ArrayList<>();
|
||||||
|
List<ChargeData> charges = new ArrayList<>();
|
||||||
|
List<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings = new ArrayList<>();
|
||||||
|
List<GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings = new ArrayList<>();
|
||||||
|
|
||||||
|
List<GetChargeOffReasonsToExpenseMappings> chargeOffReasonsToExpenseMappings = new ArrayList<>();
|
||||||
|
GetChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new GetChargeOffReasonsToExpenseMappings();
|
||||||
|
getChargeOffReasonsToExpenseMappings.setChargeOffReasonCodeValueId(chargeOffReasonId);
|
||||||
|
getChargeOffReasonsToExpenseMappings.setExpenseGLAccountId(glAccountId);
|
||||||
|
chargeOffReasonsToExpenseMappings.add(getChargeOffReasonsToExpenseMappings);
|
||||||
|
|
||||||
|
List<GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings = new ArrayList<>();
|
||||||
|
GetLoanPaymentChannelToFundSourceMappings loanPaymentChannelToFundSourceMappings = new GetLoanPaymentChannelToFundSourceMappings();
|
||||||
|
loanPaymentChannelToFundSourceMappings.fundSourceAccountId(fundSource.getAccountID().longValue());
|
||||||
|
loanPaymentChannelToFundSourceMappings.paymentTypeId(1L);
|
||||||
|
paymentChannelToFundSourceMappings.add(loanPaymentChannelToFundSourceMappings);
|
||||||
|
|
||||||
|
final Integer fundId = createFund(requestSpec, responseSpec);
|
||||||
|
Assertions.assertNotNull(fundId);
|
||||||
|
|
||||||
|
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
|
||||||
|
Assertions.assertNotNull(delinquencyBucketId);
|
||||||
|
|
||||||
|
return new PostLoanProductsRequest()//
|
||||||
|
.name(name)//
|
||||||
|
.enableAccrualActivityPosting(true)//
|
||||||
|
.shortName(shortName)//
|
||||||
|
.description(
|
||||||
|
"LP1 with 12% DECLINING BALANCE interest, interest period: Daily, Interest recalculation-Daily, Compounding:none")//
|
||||||
|
.fundId(fundId.longValue())//
|
||||||
|
.startDate(null)//
|
||||||
|
.closeDate(null)//
|
||||||
|
.includeInBorrowerCycle(false)//
|
||||||
|
.currencyCode("EUR")//
|
||||||
|
.digitsAfterDecimal(2)//
|
||||||
|
.inMultiplesOf(1)//
|
||||||
|
.installmentAmountInMultiplesOf(1)//
|
||||||
|
.useBorrowerCycle(false)//
|
||||||
|
.minPrincipal(100.0)//
|
||||||
|
.principal(1000.0)//
|
||||||
|
.maxPrincipal(10000.0)//
|
||||||
|
.minNumberOfRepayments(1)//
|
||||||
|
.numberOfRepayments(1)//
|
||||||
|
.maxNumberOfRepayments(30)//
|
||||||
|
.isLinkedToFloatingInterestRates(false)//
|
||||||
|
.minInterestRatePerPeriod(0.0)//
|
||||||
|
.interestRatePerPeriod(12.0)//
|
||||||
|
.maxInterestRatePerPeriod(30.0)//
|
||||||
|
.interestRateFrequencyType(3)//
|
||||||
|
.repaymentEvery(30)//
|
||||||
|
.repaymentFrequencyType(0L)//
|
||||||
|
.principalVariationsForBorrowerCycle(principalVariationsForBorrowerCycle)//
|
||||||
|
.numberOfRepaymentVariationsForBorrowerCycle(numberOfRepaymentVariationsForBorrowerCycle)//
|
||||||
|
.interestRateVariationsForBorrowerCycle(interestRateVariationsForBorrowerCycle)//
|
||||||
|
.amortizationType(1)//
|
||||||
|
.interestType(0)//
|
||||||
|
.isEqualAmortization(false)//
|
||||||
|
.interestCalculationPeriodType(0)//
|
||||||
|
.transactionProcessingStrategyCode("mifos-standard-strategy")//
|
||||||
|
.daysInYearType(1)//
|
||||||
|
.daysInMonthType(1)//
|
||||||
|
.canDefineInstallmentAmount(true)//
|
||||||
|
.graceOnArrearsAgeing(3)//
|
||||||
|
.overdueDaysForNPA(179)//
|
||||||
|
.accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
|
||||||
|
.principalThresholdForLastInstallment(50)//
|
||||||
|
.allowVariableInstallments(false)//
|
||||||
|
.canUseForTopup(false)//
|
||||||
|
.holdGuaranteeFunds(false)//
|
||||||
|
.multiDisburseLoan(false)//
|
||||||
|
.allowAttributeOverrides(new AllowAttributeOverrides()//
|
||||||
|
.amortizationType(true)//
|
||||||
|
.interestType(true)//
|
||||||
|
.transactionProcessingStrategyCode(true)//
|
||||||
|
.interestCalculationPeriodType(true)//
|
||||||
|
.inArrearsTolerance(true)//
|
||||||
|
.repaymentEvery(true)//
|
||||||
|
.graceOnPrincipalAndInterestPayment(true)//
|
||||||
|
.graceOnArrearsAgeing(true))
|
||||||
|
.outstandingLoanBalance(10000.0)//
|
||||||
|
.charges(charges)//
|
||||||
|
.accountingRule(3)//
|
||||||
|
|
||||||
|
.fundSourceAccountId(suspenseAccount.getAccountID().longValue())//
|
||||||
|
.loanPortfolioAccountId(loansReceivableAccount.getAccountID().longValue())//
|
||||||
|
.transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())//
|
||||||
|
.interestOnLoanAccountId(interestIncomeAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromFeeAccountId(feeIncomeAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromPenaltyAccountId(penaltyIncomeAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromRecoveryAccountId(recoveriesAccount.getAccountID().longValue())//
|
||||||
|
.writeOffAccountId(writtenOffAccount.getAccountID().longValue())//
|
||||||
|
.overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())//
|
||||||
|
.receivableInterestAccountId(interestReceivableAccount.getAccountID().longValue())//
|
||||||
|
.receivableFeeAccountId(feeReceivableAccount.getAccountID().longValue())//
|
||||||
|
.receivablePenaltyAccountId(penaltyReceivableAccount.getAccountID().longValue())//
|
||||||
|
.goodwillCreditAccountId(goodwillExpenseAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromGoodwillCreditInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromGoodwillCreditFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromGoodwillCreditPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.chargeOffExpenseAccountId(chargeOffExpenseAccount.getAccountID().longValue())//
|
||||||
|
.chargeOffFraudExpenseAccountId(chargeOffFraudExpenseAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromChargeOffPenaltyAccountId(penaltyChargeOffAccount.getAccountID().longValue())//
|
||||||
|
|
||||||
|
.dateFormat("dd MMMM yyyy")//
|
||||||
|
.locale("en")//
|
||||||
|
.disallowExpectedDisbursements(false)//
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||||
|
.delinquencyBucketId(delinquencyBucketId.longValue())//
|
||||||
|
.paymentChannelToFundSourceMappings(paymentChannelToFundSourceMappings)//
|
||||||
|
.penaltyToIncomeAccountMappings(penaltyToIncomeAccountMappings)//
|
||||||
|
.chargeOffReasonsToExpenseMappings(chargeOffReasonsToExpenseMappings).feeToIncomeAccountMappings(feeToIncomeAccountMappings)//
|
||||||
|
.isInterestRecalculationEnabled(true)//
|
||||||
|
.preClosureInterestCalculationStrategy(1)//
|
||||||
|
.rescheduleStrategyMethod(3)//
|
||||||
|
.interestRecalculationCompoundingMethod(0)//
|
||||||
|
.recalculationRestFrequencyType(2)//
|
||||||
|
.recalculationRestFrequencyInterval(1)//
|
||||||
|
.allowPartialPeriodInterestCalcualtion(false);//
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer createChargeOffReason() {
|
||||||
|
Integer chargeOffReasonId;
|
||||||
|
HashMap<String, Object> codes = CodeHelper.getCodeByName(requestSpec, responseSpec, CODE_VALUE_NAME);
|
||||||
|
if (codes.isEmpty()) {
|
||||||
|
CodeHelper.createCode(requestSpec, responseSpec, CODE_VALUE_NAME, "");
|
||||||
|
}
|
||||||
|
codes = CodeHelper.getCodeByName(requestSpec, responseSpec, CODE_VALUE_NAME);
|
||||||
|
Integer codeId = (Integer) codes.get("id");
|
||||||
|
HashMap<String, Object> codeValues = CodeHelper.getOrCreateCodeValueByCodeIdAndCodeName(requestSpec, responseSpec, codeId,
|
||||||
|
CODE_VALUE_NAME, 1);
|
||||||
|
chargeOffReasonId = (Integer) codeValues.get("id");
|
||||||
|
return chargeOffReasonId;
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -107,6 +107,7 @@ public class LoanProductTestBuilder {
|
|||||||
|
|
||||||
private List<Map<String, Long>> feeToIncomeAccountMappings = null;
|
private List<Map<String, Long>> feeToIncomeAccountMappings = null;
|
||||||
private List<Map<String, Long>> penaltyToIncomeAccountMappings = null;
|
private List<Map<String, Long>> penaltyToIncomeAccountMappings = null;
|
||||||
|
private List<Map<String, Long>> chargeOffReasonsToExpenseMappings = null;
|
||||||
private Account feeAndPenaltyAssetAccount;
|
private Account feeAndPenaltyAssetAccount;
|
||||||
|
|
||||||
private Boolean multiDisburseLoan = false;
|
private Boolean multiDisburseLoan = false;
|
||||||
@@ -303,6 +304,10 @@ public class LoanProductTestBuilder {
|
|||||||
map.put("penaltyToIncomeAccountMappings", this.penaltyToIncomeAccountMappings);
|
map.put("penaltyToIncomeAccountMappings", this.penaltyToIncomeAccountMappings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.chargeOffReasonsToExpenseMappings != null) {
|
||||||
|
map.put("chargeOffReasonsToExpenseMappings", this.chargeOffReasonsToExpenseMappings);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.dueDaysForRepaymentEvent != null) {
|
if (this.dueDaysForRepaymentEvent != null) {
|
||||||
map.put("dueDaysForRepaymentEvent", this.dueDaysForRepaymentEvent);
|
map.put("dueDaysForRepaymentEvent", this.dueDaysForRepaymentEvent);
|
||||||
}
|
}
|
||||||
@@ -796,6 +801,17 @@ public class LoanProductTestBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LoanProductTestBuilder withChargeOffReasonsToExpenseMappings(final Long reasonId, final Long accountId) {
|
||||||
|
if (this.chargeOffReasonsToExpenseMappings == null) {
|
||||||
|
this.chargeOffReasonsToExpenseMappings = new ArrayList<>();
|
||||||
|
}
|
||||||
|
Map<String, Long> newMap = new HashMap<>();
|
||||||
|
newMap.put("chargeOffReasonCodeValueId", reasonId);
|
||||||
|
newMap.put("expenseGLAccountId", accountId);
|
||||||
|
this.chargeOffReasonsToExpenseMappings.add(newMap);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public String getTransactionProcessingStrategyCode() {
|
public String getTransactionProcessingStrategyCode() {
|
||||||
return transactionProcessingStrategyCode;
|
return transactionProcessingStrategyCode;
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -93,6 +93,35 @@ public final class CodeHelper {
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static HashMap<String, Object> getOrCreateCodeValueByCodeIdAndCodeName(final RequestSpecification requestSpec,
|
||||||
|
final ResponseSpecification responseSpec, final Integer codeId, final String codeName, final Integer position) {
|
||||||
|
|
||||||
|
ArrayList<HashMap<String, Object>> allCodeValues = CodeHelper.getAllCodeValuesByCodeId(requestSpec, responseSpec, codeId);
|
||||||
|
HashMap<String, Object> codesByName = filterCodesByName(allCodeValues, codeName);
|
||||||
|
|
||||||
|
if (codesByName.isEmpty()) {
|
||||||
|
CodeHelper.createCodeValue(requestSpec, responseSpec, codeId, codeName, position);
|
||||||
|
allCodeValues = CodeHelper.getAllCodeValuesByCodeId(requestSpec, responseSpec, codeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterCodesByName(allCodeValues, codeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashMap<String, Object> filterCodesByName(ArrayList<HashMap<String, Object>> allCodeValues, String codeName) {
|
||||||
|
final HashMap<String, Object> codes = new HashMap<>();
|
||||||
|
|
||||||
|
for (HashMap<String, Object> map : allCodeValues) {
|
||||||
|
String name = (String) map.get("name");
|
||||||
|
if (name.equals(codeName)) {
|
||||||
|
codes.put("id", map.get("id"));
|
||||||
|
codes.put("name", map.get("name"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return codes;
|
||||||
|
}
|
||||||
|
|
||||||
public static HashMap<String, Object> retrieveOrCreateCodeValue(Integer codeId, final RequestSpecification requestSpec,
|
public static HashMap<String, Object> retrieveOrCreateCodeValue(Integer codeId, final RequestSpecification requestSpec,
|
||||||
final ResponseSpecification responseSpec) {
|
final ResponseSpecification responseSpec) {
|
||||||
Integer codeValueId = null;
|
Integer codeValueId = null;
|
||||||
@@ -119,6 +148,13 @@ public final class CodeHelper {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ArrayList<HashMap<String, Object>> getAllCodeValuesByCodeId(final RequestSpecification requestSpec,
|
||||||
|
final ResponseSpecification responseSpec, final Integer codeId) {
|
||||||
|
|
||||||
|
return Utils.performServerGet(requestSpec, responseSpec,
|
||||||
|
CODE_VALUE_URL.replace("[codeId]", codeId.toString()) + "?" + Utils.TENANT_IDENTIFIER, "");
|
||||||
|
}
|
||||||
|
|
||||||
public static Object getSystemDefinedCodes(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
|
public static Object getSystemDefinedCodes(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
|
||||||
|
|
||||||
final String getResponse = given().spec(requestSpec).expect().spec(responseSpec).when()
|
final String getResponse = given().spec(requestSpec).expect().spec(responseSpec).when()
|
||||||
|
|||||||
Reference in New Issue
Block a user