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)
|
||||
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,
|
||||
final int financialAccountType) {
|
||||
final int financialAccountType, final Long chargeOffReasonId) {
|
||||
|
||||
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("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,
|
||||
@Param("financialAccountType") int financialAccountType);
|
||||
|
||||
@@ -61,4 +61,8 @@ public interface ProductToGLAccountMappingRepository
|
||||
@Param("productType") 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.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.exception.ProductToGLAccountMappingInvalidException;
|
||||
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.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
@@ -53,6 +58,7 @@ import org.springframework.stereotype.Component;
|
||||
public class ProductToGLAccountMappingHelper {
|
||||
|
||||
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 ProductToGLAccountMappingRepository accountMappingRepository;
|
||||
@@ -60,6 +66,7 @@ public class ProductToGLAccountMappingHelper {
|
||||
private final ChargeRepositoryWrapper chargeRepositoryWrapper;
|
||||
protected final GLAccountRepositoryWrapper accountRepositoryWrapper;
|
||||
private final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper;
|
||||
private final CodeValueRepository codeValueRepository;
|
||||
|
||||
public void saveProductToAccountMapping(final JsonElement element, final String paramName, final Long productId,
|
||||
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 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
|
||||
*
|
||||
@@ -402,6 +489,24 @@ public class ProductToGLAccountMappingHelper {
|
||||
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() {
|
||||
List<GLAccountType> allowedAccountTypes = new ArrayList<>();
|
||||
allowedAccountTypes.add(GLAccountType.INCOME);
|
||||
@@ -455,4 +560,38 @@ public class ProductToGLAccountMappingHelper {
|
||||
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<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.GLAccountType;
|
||||
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.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||
@@ -42,9 +43,9 @@ public class SavingsProductToGLAccountMappingHelper extends ProductToGLAccountMa
|
||||
public SavingsProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository,
|
||||
final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper,
|
||||
final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper,
|
||||
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper) {
|
||||
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper, final CodeValueRepository codeValueRepository) {
|
||||
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.GLAccountType;
|
||||
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.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||
@@ -41,9 +42,9 @@ public class ShareProductToGLAccountMappingHelper extends ProductToGLAccountMapp
|
||||
public ShareProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository,
|
||||
final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper,
|
||||
final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper,
|
||||
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper) {
|
||||
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper, final CodeValueRepository codeValueRepository) {
|
||||
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_GOODWILL_CREDIT_INTEREST("incomeFromGoodwillCreditInterestAccountId"), //
|
||||
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;
|
||||
|
||||
|
||||
+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.exception.ProductToGLAccountMappingInvalidException;
|
||||
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.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||
@@ -46,9 +47,9 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
|
||||
public LoanProductToGLAccountMappingHelper(final GLAccountRepository glAccountRepository,
|
||||
final ProductToGLAccountMappingRepository glAccountMappingRepository, final FromJsonHelper fromApiJsonHelper,
|
||||
final ChargeRepositoryWrapper chargeRepositoryWrapper, final GLAccountRepositoryWrapper accountRepositoryWrapper,
|
||||
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper) {
|
||||
final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper, final CodeValueRepository codeValueRepository) {
|
||||
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);
|
||||
}
|
||||
|
||||
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,
|
||||
final Map<String, Object> changes) {
|
||||
// update both fee and penalty charges
|
||||
@@ -387,5 +398,4 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
|
||||
}
|
||||
return gLAccountType;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
@@ -246,6 +246,7 @@ final class LoanProductsApiResourceSwagger {
|
||||
public Long incomeFromGoodwillCreditPenaltyAccountId;
|
||||
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
||||
public List<GetLoanProductsProductIdResponse.GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
|
||||
public List<GetLoanProductsProductIdResponse.GetChargeOffReasonsToExpenseMappings> chargeOffReasonsToExpenseMappings;
|
||||
public List<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
||||
|
||||
// Multi Disburse
|
||||
@@ -1209,6 +1210,16 @@ final class LoanProductsApiResourceSwagger {
|
||||
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 {
|
||||
|
||||
private GetLoanFeeToIncomeAccountMappings() {}
|
||||
@@ -1319,6 +1330,7 @@ final class LoanProductsApiResourceSwagger {
|
||||
public GetLoanAccountingMappings accountingMappings;
|
||||
public Set<GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
||||
public Set<GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
|
||||
public Set<GetChargeOffReasonsToExpenseMappings> chargeOffReasonsToExpenseMappings;
|
||||
@Schema(example = "false")
|
||||
public Boolean isRatesEnabled;
|
||||
@Schema(example = "true")
|
||||
@@ -1577,6 +1589,7 @@ final class LoanProductsApiResourceSwagger {
|
||||
public Long incomeFromChargeOffPenaltyAccountId;
|
||||
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
||||
public List<GetLoanProductsProductIdResponse.GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
|
||||
public List<GetLoanProductsProductIdResponse.GetChargeOffReasonsToExpenseMappings> chargeOffReasonsToExpenseMappings;
|
||||
public List<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
||||
@Schema(example = "false")
|
||||
public Boolean enableAccrualActivityPosting;
|
||||
|
||||
+3
@@ -151,6 +151,7 @@ public class LoanProductData implements Serializable {
|
||||
private Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
|
||||
private Collection<ChargeToGLAccountMapper> feeToIncomeAccountMappings;
|
||||
private Collection<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
||||
private List<ChargeToGLAccountMapper> chargeOffReasonsToExpenseMappings;
|
||||
private final boolean enableAccrualActivityPosting;
|
||||
|
||||
// rates
|
||||
@@ -853,6 +854,7 @@ public class LoanProductData implements Serializable {
|
||||
this.paymentChannelToFundSourceMappings = null;
|
||||
this.feeToIncomeAccountMappings = null;
|
||||
this.penaltyToIncomeAccountMappings = null;
|
||||
this.chargeOffReasonsToExpenseMappings = null;
|
||||
this.valueConditionTypeOptions = null;
|
||||
this.principalVariationsForBorrowerCycle = principalVariations;
|
||||
this.interestRateVariationsForBorrowerCycle = interestRateVariations;
|
||||
@@ -992,6 +994,7 @@ public class LoanProductData implements Serializable {
|
||||
this.paymentChannelToFundSourceMappings = productData.paymentChannelToFundSourceMappings;
|
||||
this.feeToIncomeAccountMappings = productData.feeToIncomeAccountMappings;
|
||||
this.penaltyToIncomeAccountMappings = productData.penaltyToIncomeAccountMappings;
|
||||
this.chargeOffReasonsToExpenseMappings = productData.chargeOffReasonsToExpenseMappings;
|
||||
|
||||
this.chargeOptions = chargeOptions;
|
||||
this.penaltyOptions = penaltyOptions;
|
||||
|
||||
+4
@@ -130,6 +130,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
||||
// advanced accounting mappings
|
||||
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
|
||||
break;
|
||||
case ACCRUAL_UPFRONT:
|
||||
// Fall Through
|
||||
@@ -208,6 +209,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
||||
// advanced accounting mappings
|
||||
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -379,6 +381,8 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
||||
accountingRuleType);
|
||||
this.loanProductToGLAccountMappingHelper.updatePaymentChannelToFundSourceMappings(command, element, loanProductId, changes);
|
||||
this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes);
|
||||
this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId,
|
||||
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"
|
||||
+ "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n"
|
||||
+ "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n"
|
||||
+ "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, 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 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"
|
||||
@@ -336,6 +336,7 @@ public class LoanProductsApiResource {
|
||||
Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
|
||||
Collection<ChargeToGLAccountMapper> feeToGLAccountMappings;
|
||||
Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings;
|
||||
List<ChargeToGLAccountMapper> chargeOffReasonsToExpenseMappings;
|
||||
if (loanProduct.hasAccountingEnabled()) {
|
||||
accountingMappings = this.accountMappingReadPlatformService.fetchAccountMappingDetailsForLoanProduct(productId,
|
||||
loanProduct.getAccountingRule().getId().intValue());
|
||||
|
||||
+62
-1
@@ -27,6 +27,7 @@ import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -36,6 +37,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
|
||||
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.data.ApiParameterError;
|
||||
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_FEES.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.INTEREST_RATE_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_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 AdvancedPaymentAllocationsJsonParser advancedPaymentAllocationsJsonParser;
|
||||
private final AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator;
|
||||
private final ProductToGLAccountMappingHelper productToGLAccountMappingHelper;
|
||||
|
||||
public void validateForCreate(final JsonCommand command) {
|
||||
String json = command.json();
|
||||
@@ -718,6 +723,7 @@ public final class LoanProductDataValidator {
|
||||
|
||||
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
|
||||
validateChargeToIncomeAccountMappings(baseDataValidator, element);
|
||||
validateChargeOffToExpenseMappings(baseDataValidator, element);
|
||||
|
||||
}
|
||||
|
||||
@@ -1791,6 +1797,7 @@ public final class LoanProductDataValidator {
|
||||
|
||||
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
|
||||
validateChargeToIncomeAccountMappings(baseDataValidator, element);
|
||||
validateChargeOffToExpenseMappings(baseDataValidator, element);
|
||||
|
||||
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,
|
||||
final LoanProduct loanProduct) {
|
||||
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/0151_interest_refund_business_events.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>
|
||||
|
||||
+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>> penaltyToIncomeAccountMappings = null;
|
||||
private List<Map<String, Long>> chargeOffReasonsToExpenseMappings = null;
|
||||
private Account feeAndPenaltyAssetAccount;
|
||||
|
||||
private Boolean multiDisburseLoan = false;
|
||||
@@ -303,6 +304,10 @@ public class LoanProductTestBuilder {
|
||||
map.put("penaltyToIncomeAccountMappings", this.penaltyToIncomeAccountMappings);
|
||||
}
|
||||
|
||||
if (this.chargeOffReasonsToExpenseMappings != null) {
|
||||
map.put("chargeOffReasonsToExpenseMappings", this.chargeOffReasonsToExpenseMappings);
|
||||
}
|
||||
|
||||
if (this.dueDaysForRepaymentEvent != null) {
|
||||
map.put("dueDaysForRepaymentEvent", this.dueDaysForRepaymentEvent);
|
||||
}
|
||||
@@ -796,6 +801,17 @@ public class LoanProductTestBuilder {
|
||||
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() {
|
||||
return transactionProcessingStrategyCode;
|
||||
}
|
||||
|
||||
+36
@@ -93,6 +93,35 @@ public final class CodeHelper {
|
||||
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,
|
||||
final ResponseSpecification responseSpec) {
|
||||
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) {
|
||||
|
||||
final String getResponse = given().spec(requestSpec).expect().spec(responseSpec).when()
|
||||
|
||||
Reference in New Issue
Block a user