FINERACT-2358: Allow to configure advanced accounting rules based on write-off reason

This commit is contained in:
Soma Sörös
2025-10-01 10:29:03 +02:00
committed by Adam Saghy
parent 641f755193
commit d6323be7dd
20 changed files with 635 additions and 106 deletions
@@ -68,6 +68,10 @@ public class ProductToGLAccountMapping extends AbstractPersistableCustom<Long> {
@JoinColumn(name = "charge_off_reason_id", nullable = true) @JoinColumn(name = "charge_off_reason_id", nullable = true)
private CodeValue chargeOffReason; private CodeValue chargeOffReason;
@ManyToOne
@JoinColumn(name = "write_off_reason_id", nullable = true)
private CodeValue writeOffReason;
@ManyToOne @ManyToOne
@JoinColumn(name = "capitalized_income_classification_id", nullable = true) @JoinColumn(name = "capitalized_income_classification_id", nullable = true)
private CodeValue capitalizedIncomeClassification; private CodeValue capitalizedIncomeClassification;
@@ -35,7 +35,7 @@ public interface ProductToGLAccountMappingRepository
@Param("productType") int productType, @Param("financialAccountType") int financialAccountType, @Param("productType") int productType, @Param("financialAccountType") int financialAccountType,
@Param("chargeId") Long ChargeId); @Param("chargeId") Long ChargeId);
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReason is NULL and mapping.capitalizedIncomeClassification is NULL and mapping.buydownFeeClassification is NULL") @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReason is NULL and mapping.writeOffReason is NULL and mapping.capitalizedIncomeClassification is NULL and mapping.buydownFeeClassification is NULL")
ProductToGLAccountMapping findCoreProductToFinAccountMapping(@Param("productId") Long productId, @Param("productType") int productType, ProductToGLAccountMapping findCoreProductToFinAccountMapping(@Param("productId") Long productId, @Param("productType") int productType,
@Param("financialAccountType") int financialAccountType); @Param("financialAccountType") int financialAccountType);
@@ -66,11 +66,18 @@ public interface ProductToGLAccountMappingRepository
List<ProductToGLAccountMapping> findAllChargeOffReasonsMappings(@Param("productId") Long productId, List<ProductToGLAccountMapping> findAllChargeOffReasonsMappings(@Param("productId") Long productId,
@Param("productType") int productType); @Param("productType") int productType);
List<ProductToGLAccountMapping> findAllProductToGLAccountMappingsByProductIdAndProductTypeAndFinancialAccountType(Long productId,
int productType, int financialAccountType);
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.writeOffReason is not NULL")
List<ProductToGLAccountMapping> findAllWriteOffReasonsMappings(@Param("productId") Long productId,
@Param("productType") int productType);
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.chargeOffReason.id =:chargeOffReasonId AND mapping.productId =:productId AND mapping.productType =:productType") @Query("select mapping from ProductToGLAccountMapping mapping where mapping.chargeOffReason.id =:chargeOffReasonId AND mapping.productId =:productId AND mapping.productType =:productType")
ProductToGLAccountMapping findChargeOffReasonMapping(@Param("productId") Long productId, @Param("productType") Integer productType, ProductToGLAccountMapping findChargeOffReasonMapping(@Param("productId") Long productId, @Param("productType") Integer productType,
@Param("chargeOffReasonId") Long chargeOffReasonId); @Param("chargeOffReasonId") Long chargeOffReasonId);
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL AND mapping.capitalizedIncomeClassification is NULL AND mapping.buydownFeeClassification is NULL") @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL AND mapping.writeOffReason IS NULL AND mapping.capitalizedIncomeClassification is NULL AND mapping.buydownFeeClassification is NULL")
List<ProductToGLAccountMapping> findAllRegularMappings(@Param("productId") Long productId, @Param("productType") Integer productType); List<ProductToGLAccountMapping> findAllRegularMappings(@Param("productId") Long productId, @Param("productType") Integer productType);
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.paymentType is not NULL") @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.paymentType is not NULL")
@@ -29,6 +29,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan;
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
@@ -206,23 +207,25 @@ public class ProductToGLAccountMappingHelper {
} }
} }
public void saveChargeOffReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, public void saveReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
final Map<String, Object> changes, final PortfolioProductType portfolioProductType) { final Map<String, Object> changes, final PortfolioProductType portfolioProductType,
final LoanProductAccountingParams arrayNameParam, final LoanProductAccountingParams reasonCodeValueIdParam,
final CashAccountsForLoan cashAccountsForLoan) {
final String arrayName = LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(); final String arrayName = arrayNameParam.getValue();
final JsonArray chargeOffReasonToExpenseAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayName, element); final JsonArray reasonToExpenseAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayName, element);
if (chargeOffReasonToExpenseAccountMappingArray != null) { if (reasonToExpenseAccountMappingArray != null) {
if (changes != null) { if (changes != null) {
changes.put(arrayName, command.jsonFragment(arrayName)); changes.put(arrayName, command.jsonFragment(arrayName));
} }
for (int i = 0; i < chargeOffReasonToExpenseAccountMappingArray.size(); i++) { for (int i = 0; i < reasonToExpenseAccountMappingArray.size(); i++) {
final JsonObject jsonObject = chargeOffReasonToExpenseAccountMappingArray.get(i).getAsJsonObject(); final JsonObject jsonObject = reasonToExpenseAccountMappingArray.get(i).getAsJsonObject();
final Long reasonId = jsonObject.get(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue()).getAsLong(); final Long reasonId = jsonObject.get(reasonCodeValueIdParam.getValue()).getAsLong();
final Long expenseAccountId = jsonObject.get(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()).getAsLong(); final Long expenseAccountId = jsonObject.get(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()).getAsLong();
saveChargeOffReasonToExpenseMapping(productId, reasonId, expenseAccountId, portfolioProductType); saveReasonToExpenseMapping(productId, reasonId, expenseAccountId, portfolioProductType, cashAccountsForLoan);
} }
} }
} }
@@ -413,60 +416,74 @@ public class ProductToGLAccountMappingHelper {
} }
} }
public void updateChargeOffReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, private Long getReasonIdByCashAccountForLoan(final ProductToGLAccountMapping productToGLAccountMapping,
final Map<String, Object> changes, final PortfolioProductType portfolioProductType) { final CashAccountsForLoan cashAccountsForLoan) {
return switch (cashAccountsForLoan) {
case LOSSES_WRITTEN_OFF -> productToGLAccountMapping != null && productToGLAccountMapping.getWriteOffReason() != null
? productToGLAccountMapping.getWriteOffReason().getId()
: null;
case CHARGE_OFF_EXPENSE -> productToGLAccountMapping != null && productToGLAccountMapping.getChargeOffReason() != null
? productToGLAccountMapping.getChargeOffReason().getId()
: null;
default -> throw new IllegalStateException("Unexpected value: " + cashAccountsForLoan);
};
}
final List<ProductToGLAccountMapping> existingChargeOffReasonToGLAccountMappings = this.accountMappingRepository public void updateReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
.findAllChargeOffReasonsMappings(productId, portfolioProductType.getValue()); final Map<String, Object> changes, final PortfolioProductType portfolioProductType,
final JsonArray chargeOffReasonToGLAccountMappingArray = this.fromApiJsonHelper final List<ProductToGLAccountMapping> existingReasonToGLAccountMappings,
.extractJsonArrayNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(), element); final LoanProductAccountingParams reasonToExpenseAccountMappingsParam, final LoanProductAccountingParams reasonCodeValueIdParam,
final CashAccountsForLoan cashAccountsForLoan) {
final Map<Long, Long> inputChargeOffReasonToGLAccountMap = new HashMap<>(); final JsonArray reasonToGLAccountMappingArray = this.fromApiJsonHelper
.extractJsonArrayNamed(reasonToExpenseAccountMappingsParam.getValue(), element);
final Set<Long> existingChargeOffReasons = new HashSet<>(); final Map<Long, Long> inputReasonToGLAccountMap = new HashMap<>();
if (chargeOffReasonToGLAccountMappingArray != null) {
final Set<Long> existingReasons = new HashSet<>();
if (reasonToGLAccountMappingArray != null) {
if (changes != null) { if (changes != null) {
changes.put(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(), changes.put(reasonToExpenseAccountMappingsParam.getValue(),
command.jsonFragment(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue())); command.jsonFragment(reasonToExpenseAccountMappingsParam.getValue()));
} }
for (int i = 0; i < chargeOffReasonToGLAccountMappingArray.size(); i++) { for (int i = 0; i < reasonToGLAccountMappingArray.size(); i++) {
final JsonObject jsonObject = chargeOffReasonToGLAccountMappingArray.get(i).getAsJsonObject(); final JsonObject jsonObject = reasonToGLAccountMappingArray.get(i).getAsJsonObject();
final Long expenseGlAccountId = jsonObject.get(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()).getAsLong(); final Long expenseGlAccountId = jsonObject.get(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()).getAsLong();
final Long chargeOffReasonCodeValueId = jsonObject final Long reasonCodeValueId = jsonObject.get(reasonCodeValueIdParam.getValue()).getAsLong();
.get(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue()).getAsLong(); inputReasonToGLAccountMap.put(reasonCodeValueId, expenseGlAccountId);
inputChargeOffReasonToGLAccountMap.put(chargeOffReasonCodeValueId, expenseGlAccountId);
} }
// If input map is empty, delete all existing mappings // If input map is empty, delete all existing mappings
if (inputChargeOffReasonToGLAccountMap.isEmpty()) { if (inputReasonToGLAccountMap.isEmpty()) {
this.accountMappingRepository.deleteAll(existingChargeOffReasonToGLAccountMappings); this.accountMappingRepository.deleteAll(existingReasonToGLAccountMappings);
} else { } else {
for (final ProductToGLAccountMapping existingChargeOffReasonToGLAccountMapping : existingChargeOffReasonToGLAccountMappings) { for (final ProductToGLAccountMapping existingReasonToGLAccountMapping : existingReasonToGLAccountMappings) {
final Long currentChargeOffReasonId = existingChargeOffReasonToGLAccountMapping.getChargeOffReason().getId(); final Long currentReasonId = getReasonIdByCashAccountForLoan(existingReasonToGLAccountMapping, cashAccountsForLoan);
if (currentChargeOffReasonId != null) { if (currentReasonId != null) {
existingChargeOffReasons.add(currentChargeOffReasonId); existingReasons.add(currentReasonId);
// update existing mappings (if required) // update existing mappings (if required)
if (inputChargeOffReasonToGLAccountMap.containsKey(currentChargeOffReasonId)) { if (inputReasonToGLAccountMap.containsKey(currentReasonId)) {
final Long newGLAccountId = inputChargeOffReasonToGLAccountMap.get(currentChargeOffReasonId); final Long newGLAccountId = inputReasonToGLAccountMap.get(currentReasonId);
if (!newGLAccountId.equals(existingChargeOffReasonToGLAccountMapping.getGlAccount().getId())) { if (!newGLAccountId.equals(existingReasonToGLAccountMapping.getGlAccount().getId())) {
final Optional<GLAccount> glAccount = accountRepository.findById(newGLAccountId); final Optional<GLAccount> glAccount = accountRepository.findById(newGLAccountId);
if (glAccount.isPresent()) { if (glAccount.isPresent()) {
existingChargeOffReasonToGLAccountMapping.setGlAccount(glAccount.get()); existingReasonToGLAccountMapping.setGlAccount(glAccount.get());
this.accountMappingRepository.saveAndFlush(existingChargeOffReasonToGLAccountMapping); this.accountMappingRepository.saveAndFlush(existingReasonToGLAccountMapping);
} }
} }
} // deleted payment type } // deleted payment type
else { else {
this.accountMappingRepository.delete(existingChargeOffReasonToGLAccountMapping); this.accountMappingRepository.delete(existingReasonToGLAccountMapping);
} }
} }
} }
// only the newly added // only the newly added
for (Map.Entry<Long, Long> entry : inputChargeOffReasonToGLAccountMap.entrySet().stream() for (Map.Entry<Long, Long> entry : inputReasonToGLAccountMap.entrySet().stream()
.filter(e -> !existingChargeOffReasons.contains(e.getKey())).toList()) { .filter(e -> !existingReasons.contains(e.getKey())).toList()) {
saveChargeOffReasonToExpenseMapping(productId, entry.getKey(), entry.getValue(), portfolioProductType); saveReasonToExpenseMapping(productId, entry.getKey(), entry.getValue(), portfolioProductType, cashAccountsForLoan);
} }
} }
} }
@@ -587,21 +604,37 @@ public class ProductToGLAccountMappingHelper {
this.accountMappingRepository.saveAndFlush(accountMapping); this.accountMappingRepository.saveAndFlush(accountMapping);
} }
private void saveChargeOffReasonToExpenseMapping(final Long productId, final Long reasonId, final Long expenseAccountId, private Predicate<? super ProductToGLAccountMapping> matching(final CashAccountsForLoan typeDef, final Long reasonId) {
final PortfolioProductType portfolioProductType) { return switch (typeDef) {
case CHARGE_OFF_EXPENSE -> (mapping) -> (mapping.getChargeOffReason() != null && mapping.getChargeOffReason().getId() != null
&& mapping.getChargeOffReason().getId().equals(reasonId));
case LOSSES_WRITTEN_OFF -> (mapping) -> (mapping.getWriteOffReason() != null && mapping.getWriteOffReason().getId() != null
&& mapping.getWriteOffReason().getId().equals(reasonId));
default -> throw new IllegalStateException("Unexpected value: " + typeDef);
};
}
private void saveReasonToExpenseMapping(final Long productId, final Long reasonId, final Long expenseAccountId,
final PortfolioProductType portfolioProductType, final CashAccountsForLoan cashAccountsForLoan) {
final Optional<GLAccount> glAccount = accountRepository.findById(expenseAccountId); final Optional<GLAccount> glAccount = accountRepository.findById(expenseAccountId);
final boolean reasonMappingExists = this.accountMappingRepository
.findAllChargeOffReasonsMappings(productId, portfolioProductType.getValue()).stream()
.anyMatch(mapping -> mapping.getChargeOffReason().getId().equals(reasonId));
final Optional<CodeValue> codeValueOptional = codeValueRepository.findById(reasonId); final Optional<CodeValue> codeValueOptional = codeValueRepository.findById(reasonId);
if (glAccount.isPresent() && !reasonMappingExists && codeValueOptional.isPresent()) { final boolean reasonMappingExists = this.accountMappingRepository
.findAllProductToGLAccountMappingsByProductIdAndProductTypeAndFinancialAccountType(productId,
portfolioProductType.getValue(), cashAccountsForLoan.getValue())
.stream().anyMatch(matching(cashAccountsForLoan, reasonId));
if (!reasonMappingExists && glAccount.isPresent() && codeValueOptional.isPresent()) {
final ProductToGLAccountMapping accountMapping = new ProductToGLAccountMapping().setGlAccount(glAccount.get()) final ProductToGLAccountMapping accountMapping = new ProductToGLAccountMapping().setGlAccount(glAccount.get())
.setProductId(productId).setProductType(portfolioProductType.getValue()) .setProductId(productId).setProductType(portfolioProductType.getValue())
.setFinancialAccountType(CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue()).setChargeOffReason(codeValueOptional.get()); .setFinancialAccountType(cashAccountsForLoan.getValue());
switch (cashAccountsForLoan) {
case CHARGE_OFF_EXPENSE -> accountMapping.setChargeOffReason(codeValueOptional.get());
case LOSSES_WRITTEN_OFF -> accountMapping.setWriteOffReason(codeValueOptional.get());
default -> throw new IllegalStateException("Unexpected value: " + cashAccountsForLoan);
}
this.accountMappingRepository.saveAndFlush(accountMapping); this.accountMappingRepository.saveAndFlush(accountMapping);
} }
@@ -703,22 +736,25 @@ public class ProductToGLAccountMappingHelper {
} }
} }
public void validateChargeOffMappingsInDatabase(final List<JsonObject> mappings) { public void validateWriteOffMappingsInDatabase(final List<ApiParameterError> validationErrors, final List<JsonObject> mappings) {
final List<ApiParameterError> validationErrors = new ArrayList<>(); for (JsonObject jsonObject : mappings) {
final Long writeOffReasonCodeValueId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.WRITE_OFF_REASON_CODE_VALUE_ID.getValue(), jsonObject);
// Validation: writeOffReasonCodeValueId must exist in the database
CodeValue codeValue = this.codeValueRepository.findByCodeNameAndId("WriteOffReasons", writeOffReasonCodeValueId);
if (codeValue == null) {
validationErrors.add(ApiParameterError.parameterError("validation.msg.writeoffreason.invalid",
"Write-off reason with ID " + writeOffReasonCodeValueId + " does not exist",
LoanProductAccountingParams.WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue()));
}
}
}
public void validateGLAccountInDatabase(final List<ApiParameterError> validationErrors, final List<JsonObject> mappings) {
for (JsonObject jsonObject : mappings) { for (JsonObject jsonObject : mappings) {
final Long expenseGlAccountId = this.fromApiJsonHelper final Long expenseGlAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject); .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_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue()));
}
// Validation: expenseGLAccountId must exist as a valid Expense GL account // Validation: expenseGLAccountId must exist as a valid Expense GL account
final Optional<GLAccount> glAccount = accountRepository.findById(expenseGlAccountId); final Optional<GLAccount> glAccount = accountRepository.findById(expenseGlAccountId);
@@ -730,6 +766,22 @@ public class ProductToGLAccountMappingHelper {
} }
} }
}
public void validateChargeOffMappingsInDatabase(final List<ApiParameterError> validationErrors, final List<JsonObject> mappings) {
for (JsonObject jsonObject : mappings) {
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_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue()));
}
}
// Throw all collected validation errors, if any // Throw all collected validation errors, if any
if (!validationErrors.isEmpty()) { if (!validationErrors.isEmpty()) {
@@ -25,6 +25,7 @@ import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReas
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData;
import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.WriteOffReasonsToExpenseAccountMapper;
public interface ProductToGLAccountMappingReadPlatformService { public interface ProductToGLAccountMappingReadPlatformService {
@@ -52,6 +53,8 @@ public interface ProductToGLAccountMappingReadPlatformService {
List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId); List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId);
List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappingsForLoanProduct(Long loanProductId);
List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId, List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId,
LoanProductAccountingParams classificationParameter); LoanProductAccountingParams classificationParameter);
} }
@@ -40,6 +40,7 @@ import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReas
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData;
import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.WriteOffReasonsToExpenseAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping; import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository; import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.data.CodeValueData;
@@ -293,6 +294,22 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
return chargeOffReasonToGLAccountMappers; return chargeOffReasonToGLAccountMappers;
} }
private List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappings(final PortfolioProductType portfolioProductType,
final Long loanProductId) {
final List<ProductToGLAccountMapping> mappings = productToGLAccountMappingRepository.findAllWriteOffReasonsMappings(loanProductId,
portfolioProductType.getValue());
List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseAccountMappers = mappings.isEmpty() ? null : new ArrayList<>();
for (final ProductToGLAccountMapping mapping : mappings) {
final String glCode = String.valueOf(mapping.getGlAccount().getId());
final String writeOffReasonId = String.valueOf(mapping.getWriteOffReason().getId());
final WriteOffReasonsToExpenseAccountMapper writeOffReasonToGLAccountMapper = new WriteOffReasonsToExpenseAccountMapper()
.setWriteOffReasonCodeValueId(writeOffReasonId).setExpenseAccountId(glCode);
writeOffReasonsToExpenseAccountMappers.add(writeOffReasonToGLAccountMapper);
}
return writeOffReasonsToExpenseAccountMappers;
}
private List<ClassificationToGLAccountData> fetchClassificationMappings(final PortfolioProductType portfolioProductType, private List<ClassificationToGLAccountData> fetchClassificationMappings(final PortfolioProductType portfolioProductType,
final Long loanProductId, LoanProductAccountingParams classificationParameter) { final Long loanProductId, LoanProductAccountingParams classificationParameter) {
final List<ProductToGLAccountMapping> mappings = classificationParameter final List<ProductToGLAccountMapping> mappings = classificationParameter
@@ -367,6 +384,11 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
return fetchChargeOffReasonMappings(PortfolioProductType.LOAN, loanProductId); return fetchChargeOffReasonMappings(PortfolioProductType.LOAN, loanProductId);
} }
@Override
public List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappingsForLoanProduct(Long loanProductId) {
return fetchWriteOffReasonMappings(PortfolioProductType.LOAN, loanProductId);
}
@Override @Override
public List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId, public List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId,
LoanProductAccountingParams classificationParameter) { LoanProductAccountingParams classificationParameter) {
@@ -181,8 +181,10 @@ public final class AccountingConstants {
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_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS("chargeOffReasonToExpenseAccountMappings"), // CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS("chargeOffReasonToExpenseAccountMappings"), //
WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS("writeOffReasonsToExpenseMappings"), //
EXPENSE_GL_ACCOUNT_ID("expenseAccountId"), // EXPENSE_GL_ACCOUNT_ID("expenseAccountId"), //
CHARGE_OFF_REASON_CODE_VALUE_ID("chargeOffReasonCodeValueId"), // CHARGE_OFF_REASON_CODE_VALUE_ID("chargeOffReasonCodeValueId"), //
WRITE_OFF_REASON_CODE_VALUE_ID("writeOffReasonCodeValueId"), //
DEFERRED_INCOME_LIABILITY("deferredIncomeLiabilityAccountId"), // DEFERRED_INCOME_LIABILITY("deferredIncomeLiabilityAccountId"), //
INCOME_FROM_CAPITALIZATION("incomeFromCapitalizationAccountId"), // INCOME_FROM_CAPITALIZATION("incomeFromCapitalizationAccountId"), //
BUY_DOWN_EXPENSE("buyDownExpenseAccountId"), // BUY_DOWN_EXPENSE("buyDownExpenseAccountId"), //
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.producttoaccountmapping.data;
import java.io.Serial;
import java.io.Serializable;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class WriteOffReasonsToExpenseAccountMapper implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String writeOffReasonCodeValueId;
private String expenseAccountId;
}
@@ -531,13 +531,20 @@ public class DataValidatorBuilder {
} }
if (this.value != null) { if (this.value != null) {
final long number = Long.parseLong(this.value.toString()); try {
if (number < 1) { final long number = Long.parseLong(this.value.toString());
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".not.greater.than.zero"; if (number < 1) {
String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be greater than 0."; String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".not.greater.than.zero";
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage, this.parameter, String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be greater than 0.";
number, 0); final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage,
this.dataValidationErrors.add(error); this.parameter, number, 0);
this.dataValidationErrors.add(error);
}
} catch (NumberFormatException e) {
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".not.a.number";
String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be a number.";
this.dataValidationErrors.add(ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage, this.parameter));
throwValidationErrors();
} }
} }
return this; return this;
@@ -30,6 +30,7 @@ import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository; 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.ProductToGLAccountMapping;
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;
@@ -141,12 +142,39 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
public void saveChargeOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, public void saveChargeOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
final Map<String, Object> changes) { final Map<String, Object> changes) {
saveChargeOffReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN); saveReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN,
LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS,
LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID, CashAccountsForLoan.CHARGE_OFF_EXPENSE);
}
public void saveWriteOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
final Map<String, Object> changes) {
saveReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN,
LoanProductAccountingParams.WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS,
LoanProductAccountingParams.WRITE_OFF_REASON_CODE_VALUE_ID, CashAccountsForLoan.LOSSES_WRITTEN_OFF);
}
public void updateWriteOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
final Map<String, Object> changes) {
final List<ProductToGLAccountMapping> existingWriteOffReasonToGLAccountMappings = this.accountMappingRepository
.findAllWriteOffReasonsMappings(productId, PortfolioProductType.LOAN.getValue());
LoanProductAccountingParams reasonToExpenseAccountMappingsParam = LoanProductAccountingParams.WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS;
LoanProductAccountingParams reasonCodeValueIdParam = LoanProductAccountingParams.WRITE_OFF_REASON_CODE_VALUE_ID;
CashAccountsForLoan cashAccountsForLoan = CashAccountsForLoan.LOSSES_WRITTEN_OFF;
updateReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN,
existingWriteOffReasonToGLAccountMappings, reasonToExpenseAccountMappingsParam, reasonCodeValueIdParam,
cashAccountsForLoan);
} }
public void updateChargeOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId, public void updateChargeOffReasonToExpenseAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
final Map<String, Object> changes) { final Map<String, Object> changes) {
updateChargeOffReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN); final List<ProductToGLAccountMapping> chargeOffReasonsMappings = this.accountMappingRepository
.findAllChargeOffReasonsMappings(productId, PortfolioProductType.LOAN.getValue());
LoanProductAccountingParams reasonToExpenseAccountMappingsParam = LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS;
LoanProductAccountingParams reasonCodeValueIdParam = LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID;
CashAccountsForLoan cashAccountsForLoan = CashAccountsForLoan.CHARGE_OFF_EXPENSE;
updateReasonToGLAccountMappings(command, element, productId, changes, PortfolioProductType.LOAN, chargeOffReasonsMappings,
reasonToExpenseAccountMappingsParam, reasonCodeValueIdParam, cashAccountsForLoan);
} }
public void saveCapitalizedIncomeClassificationToIncomeAccountMappings(final JsonCommand command, final JsonElement element, public void saveCapitalizedIncomeClassificationToIncomeAccountMappings(final JsonCommand command, final JsonElement element,
@@ -301,6 +301,7 @@ public final class LoanProductsApiResourceSwagger {
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings; public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings; public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings;
public List<PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings; public List<PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings;
public List<PostWriteOffReasonToExpenseAccountMappings> writeOffReasonsToExpenseMappings;
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> buydownfeeClassificationToIncomeAccountMappings; public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> buydownfeeClassificationToIncomeAccountMappings;
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings; public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings;
public List<LoanProductChargeToGLAccountMapper> penaltyToIncomeAccountMappings; public List<LoanProductChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
@@ -372,7 +373,7 @@ public final class LoanProductsApiResourceSwagger {
@Schema(example = "REGULAR") @Schema(example = "REGULAR")
public String chargeOffBehaviour; public String chargeOffBehaviour;
static final class PostChargeOffReasonToExpenseAccountMappings { public static final class PostChargeOffReasonToExpenseAccountMappings {
private PostChargeOffReasonToExpenseAccountMappings() {} private PostChargeOffReasonToExpenseAccountMappings() {}
@@ -382,6 +383,17 @@ public final class LoanProductsApiResourceSwagger {
public Long expenseAccountId; public Long expenseAccountId;
} }
@Schema(description = "PostWriteOffReasonToExpenseAccountMappings")
public static final class PostWriteOffReasonToExpenseAccountMappings {
private PostWriteOffReasonToExpenseAccountMappings() {}
@Schema(example = "1")
public String writeOffReasonCodeValueId;
@Schema(example = "1")
public String expenseAccountId;
}
static final class PostClassificationToIncomeAccountMappings { static final class PostClassificationToIncomeAccountMappings {
private PostClassificationToIncomeAccountMappings() {} private PostClassificationToIncomeAccountMappings() {}
@@ -1154,6 +1166,7 @@ public final class LoanProductsApiResourceSwagger {
public List<StringEnumOptionData> supportedInterestRefundTypes; public List<StringEnumOptionData> supportedInterestRefundTypes;
public List<StringEnumOptionData> supportedInterestRefundTypesOptions; public List<StringEnumOptionData> supportedInterestRefundTypesOptions;
public List<GetLoanProductsChargeOffReasonOptions> chargeOffReasonOptions; public List<GetLoanProductsChargeOffReasonOptions> chargeOffReasonOptions;
public List<GetLoanProductsWriteOffReasonOptions> writeOffReasonOptions;
public StringEnumOptionData chargeOffBehaviour; public StringEnumOptionData chargeOffBehaviour;
public List<StringEnumOptionData> chargeOffBehaviourOptions; public List<StringEnumOptionData> chargeOffBehaviourOptions;
@Schema(example = "false") @Schema(example = "false")
@@ -1380,6 +1393,17 @@ public final class LoanProductsApiResourceSwagger {
public Long incomeAccountId; public Long incomeAccountId;
} }
@Schema(description = "GetWriteOffReasonToExpenseAccountMappings")
public static final class GetWriteOffReasonToExpenseAccountMappings {
private GetWriteOffReasonToExpenseAccountMappings() {}
@Schema(example = "1")
public String writeOffReasonCodeValueId;
@Schema(example = "1")
public String expenseAccountId;
}
@Schema(example = "11") @Schema(example = "11")
public Long id; public Long id;
@Schema(example = "advanced accounting") @Schema(example = "advanced accounting")
@@ -1468,6 +1492,7 @@ public final class LoanProductsApiResourceSwagger {
public Set<GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings; public Set<GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
public Set<GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings; public Set<GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
public List<GetChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings; public List<GetChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings;
public List<PostLoanProductsRequest.PostWriteOffReasonToExpenseAccountMappings> writeOffReasonsToExpenseMappings;
@Schema(example = "false") @Schema(example = "false")
public Boolean isRatesEnabled; public Boolean isRatesEnabled;
@Schema(example = "true") @Schema(example = "true")
@@ -1511,6 +1536,7 @@ public final class LoanProductsApiResourceSwagger {
public Boolean enableAccrualActivityPosting; public Boolean enableAccrualActivityPosting;
public List<StringEnumOptionData> supportedInterestRefundTypes; public List<StringEnumOptionData> supportedInterestRefundTypes;
public List<GetLoanProductsChargeOffReasonOptions> chargeOffReasonOptions; public List<GetLoanProductsChargeOffReasonOptions> chargeOffReasonOptions;
public List<GetLoanProductsWriteOffReasonOptions> writeOffReasonOptions;
public StringEnumOptionData chargeOffBehaviour; public StringEnumOptionData chargeOffBehaviour;
@Schema(example = "false") @Schema(example = "false")
public Boolean interestRecognitionOnDisbursementDate; public Boolean interestRecognitionOnDisbursementDate;
@@ -1774,6 +1800,7 @@ public final class LoanProductsApiResourceSwagger {
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings; public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings; public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings;
public List<PostLoanProductsRequest.PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings; public List<PostLoanProductsRequest.PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings;
public List<PostLoanProductsRequest.PostWriteOffReasonToExpenseAccountMappings> writeOffReasonsToExpenseMappings;
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> buydownfeeClassificationToIncomeAccountMappings; public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> buydownfeeClassificationToIncomeAccountMappings;
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings; public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings;
public List<LoanProductChargeToGLAccountMapper> penaltyToIncomeAccountMappings; public List<LoanProductChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
@@ -1904,4 +1931,24 @@ public final class LoanProductsApiResourceSwagger {
@Schema(example = "false") @Schema(example = "false")
public Boolean mandatory; public Boolean mandatory;
} }
@Schema(description = "GetLoanProductsWriteOffReasonOptions")
public static final class GetLoanProductsWriteOffReasonOptions {
private GetLoanProductsWriteOffReasonOptions() {}
@Schema(example = "2")
public Long id;
@Schema(example = "debit_card")
public String name;
@Schema(example = "2")
public Integer position;
@Schema(example = "Write-Off reason description")
public String description;
@Schema(example = "true")
public Boolean active;
@Schema(example = "false")
public Boolean mandatory;
}
} }
@@ -34,6 +34,7 @@ import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReas
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData;
import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.WriteOffReasonsToExpenseAccountMapper;
import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.api.ApiFacingEnum; import org.apache.fineract.infrastructure.core.api.ApiFacingEnum;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.EnumOptionData;
@@ -165,7 +166,8 @@ public class LoanProductData implements Serializable {
private Collection<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings; private Collection<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
private List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToExpenseAccountMappings; private List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToExpenseAccountMappings;
private final boolean enableAccrualActivityPosting; private final boolean enableAccrualActivityPosting;
private List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings;
private final List<CodeValueData> writeOffReasonOptions;
// rates // rates
private final boolean isRatesEnabled; private final boolean isRatesEnabled;
private final Collection<RateData> rates; private final Collection<RateData> rates;
@@ -379,6 +381,8 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeStrategy = null; final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null; final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false; final boolean merchantBuyDownFee = false;
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings = null;
final List<CodeValueData> writeOffReasonOptions = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -402,7 +406,8 @@ public class LoanProductData implements Serializable {
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization, interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee, capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee); buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee, writeOffReasonsToExpenseMappings,
writeOffReasonOptions);
} }
@@ -516,6 +521,8 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeStrategy = null; final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null; final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false; final boolean merchantBuyDownFee = false;
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings = null;
final List<CodeValueData> writeOffReasonOptions = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -539,7 +546,8 @@ public class LoanProductData implements Serializable {
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization, interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee, capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee); buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee, writeOffReasonsToExpenseMappings,
writeOffReasonOptions);
} }
@@ -660,6 +668,8 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeStrategy = null; final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null; final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false; final boolean merchantBuyDownFee = false;
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings = null;
final List<CodeValueData> writeOffReasonOptions = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -683,7 +693,8 @@ public class LoanProductData implements Serializable {
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization, interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee, capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee); buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee, writeOffReasonsToExpenseMappings,
writeOffReasonOptions);
} }
@@ -798,6 +809,8 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeStrategy = null; final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null; final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false; final boolean merchantBuyDownFee = false;
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings = null;
final List<CodeValueData> writeOffReasonOptions = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -821,7 +834,8 @@ public class LoanProductData implements Serializable {
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization, interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee, capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee); buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee, writeOffReasonsToExpenseMappings,
writeOffReasonOptions);
} }
public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings, public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings,
@@ -829,6 +843,7 @@ public class LoanProductData implements Serializable {
final Collection<ChargeToGLAccountMapper> feeToGLAccountMappings, final Collection<ChargeToGLAccountMapper> feeToGLAccountMappings,
final Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings, final Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings,
final List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings, final List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings,
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonToGLAccountMappings,
final List<ClassificationToGLAccountData> capitalizedIncomeClassificationToIncomeAccountMappings, final List<ClassificationToGLAccountData> capitalizedIncomeClassificationToIncomeAccountMappings,
final List<ClassificationToGLAccountData> buydownFeeClassificationToIncomeAccountMappings) { final List<ClassificationToGLAccountData> buydownFeeClassificationToIncomeAccountMappings) {
productData.accountingMappings = accountingMappings; productData.accountingMappings = accountingMappings;
@@ -836,6 +851,7 @@ public class LoanProductData implements Serializable {
productData.feeToIncomeAccountMappings = feeToGLAccountMappings; productData.feeToIncomeAccountMappings = feeToGLAccountMappings;
productData.penaltyToIncomeAccountMappings = penaltyToGLAccountMappings; productData.penaltyToIncomeAccountMappings = penaltyToGLAccountMappings;
productData.chargeOffReasonToExpenseAccountMappings = chargeOffReasonToGLAccountMappings; productData.chargeOffReasonToExpenseAccountMappings = chargeOffReasonToGLAccountMappings;
productData.writeOffReasonsToExpenseMappings = writeOffReasonToGLAccountMappings;
productData.capitalizedIncomeClassificationToIncomeAccountMappings = capitalizedIncomeClassificationToIncomeAccountMappings; productData.capitalizedIncomeClassificationToIncomeAccountMappings = capitalizedIncomeClassificationToIncomeAccountMappings;
productData.buydownFeeClassificationToIncomeAccountMappings = buydownFeeClassificationToIncomeAccountMappings; productData.buydownFeeClassificationToIncomeAccountMappings = buydownFeeClassificationToIncomeAccountMappings;
return productData; return productData;
@@ -884,7 +900,9 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData capitalizedIncomeCalculationType, final StringEnumOptionData capitalizedIncomeStrategy, final StringEnumOptionData capitalizedIncomeCalculationType, final StringEnumOptionData capitalizedIncomeStrategy,
final StringEnumOptionData capitalizedIncomeType, final boolean enableBuyDownFee, final StringEnumOptionData capitalizedIncomeType, final boolean enableBuyDownFee,
final StringEnumOptionData buyDownFeeCalculationType, final StringEnumOptionData buyDownFeeStrategy, final StringEnumOptionData buyDownFeeCalculationType, final StringEnumOptionData buyDownFeeStrategy,
final StringEnumOptionData buyDownFeeIncomeType, final boolean merchantBuyDownFee) { final StringEnumOptionData buyDownFeeIncomeType, final boolean merchantBuyDownFee,
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings,
final List<CodeValueData> writeOffReasonOptions) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.shortName = shortName; this.shortName = shortName;
@@ -971,6 +989,7 @@ public class LoanProductData implements Serializable {
this.feeToIncomeAccountMappings = null; this.feeToIncomeAccountMappings = null;
this.penaltyToIncomeAccountMappings = null; this.penaltyToIncomeAccountMappings = null;
this.chargeOffReasonToExpenseAccountMappings = null; this.chargeOffReasonToExpenseAccountMappings = null;
this.writeOffReasonsToExpenseMappings = null;
this.valueConditionTypeOptions = null; this.valueConditionTypeOptions = null;
this.principalVariationsForBorrowerCycle = principalVariations; this.principalVariationsForBorrowerCycle = principalVariations;
this.interestRateVariationsForBorrowerCycle = interestRateVariations; this.interestRateVariationsForBorrowerCycle = interestRateVariations;
@@ -1046,6 +1065,8 @@ public class LoanProductData implements Serializable {
this.buyDownFeeCalculationTypeOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeCalculationType.class); this.buyDownFeeCalculationTypeOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeCalculationType.class);
this.buyDownFeeStrategyOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class); this.buyDownFeeStrategyOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class);
this.buyDownFeeIncomeTypeOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class); this.buyDownFeeIncomeTypeOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class);
this.writeOffReasonsToExpenseMappings = writeOffReasonsToExpenseMappings;
this.writeOffReasonOptions = writeOffReasonOptions;
this.capitalizedIncomeClassificationOptions = null; this.capitalizedIncomeClassificationOptions = null;
this.buydownFeeClassificationOptions = null; this.buydownFeeClassificationOptions = null;
this.capitalizedIncomeClassificationToIncomeAccountMappings = null; this.capitalizedIncomeClassificationToIncomeAccountMappings = null;
@@ -1079,8 +1100,8 @@ public class LoanProductData implements Serializable {
final List<StringEnumOptionData> capitalizedIncomeStrategyOptions, final List<StringEnumOptionData> capitalizedIncomeStrategyOptions,
final List<StringEnumOptionData> capitalizedIncomeTypeOptions, final List<StringEnumOptionData> capitalizedIncomeTypeOptions,
final List<StringEnumOptionData> buyDownFeeCalculationTypeOptions, final List<StringEnumOptionData> buyDownFeeStrategyOptions, final List<StringEnumOptionData> buyDownFeeCalculationTypeOptions, final List<StringEnumOptionData> buyDownFeeStrategyOptions,
final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions, final List<CodeValueData> capitalizedIncomeClassificationOptions, final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions, final List<CodeValueData> writeOffReasonOptions,
final List<CodeValueData> buydownFeeClassificationOptions) { final List<CodeValueData> capitalizedIncomeClassificationOptions, final List<CodeValueData> buydownFeeClassificationOptions) {
this.id = productData.id; this.id = productData.id;
this.name = productData.name; this.name = productData.name;
@@ -1134,6 +1155,8 @@ public class LoanProductData implements Serializable {
this.feeToIncomeAccountMappings = productData.feeToIncomeAccountMappings; this.feeToIncomeAccountMappings = productData.feeToIncomeAccountMappings;
this.penaltyToIncomeAccountMappings = productData.penaltyToIncomeAccountMappings; this.penaltyToIncomeAccountMappings = productData.penaltyToIncomeAccountMappings;
this.chargeOffReasonToExpenseAccountMappings = productData.chargeOffReasonToExpenseAccountMappings; this.chargeOffReasonToExpenseAccountMappings = productData.chargeOffReasonToExpenseAccountMappings;
this.writeOffReasonsToExpenseMappings = productData.writeOffReasonsToExpenseMappings;
this.writeOffReasonOptions = writeOffReasonOptions;
this.chargeOptions = chargeOptions; this.chargeOptions = chargeOptions;
this.penaltyOptions = penaltyOptions; this.penaltyOptions = penaltyOptions;
@@ -140,6 +140,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveWriteOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element, this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element,
loanProductId, null); loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element, this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
@@ -237,6 +238,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null); this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveWriteOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element, this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element,
loanProductId, null); loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element, this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
@@ -419,6 +421,8 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes); this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes);
this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId,
changes); changes);
this.loanProductToGLAccountMappingHelper.updateWriteOffReasonToExpenseAccountMappings(command, element, loanProductId, changes);
this.loanProductToGLAccountMappingHelper.updateBuyDownFeeClassificationToIncomeAccountMappings(command, element, loanProductId, this.loanProductToGLAccountMappingHelper.updateBuyDownFeeClassificationToIncomeAccountMappings(command, element, loanProductId,
changes); changes);
this.loanProductToGLAccountMappingHelper.updateCapitalizedIncomeClassificationToIncomeAccountMappings(command, element, this.loanProductToGLAccountMappingHelper.updateCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
@@ -53,6 +53,7 @@ import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReas
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData; import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData;
import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper; import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.WriteOffReasonsToExpenseAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingReadPlatformService; import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingReadPlatformService;
import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.CommandWrapperBuilder;
@@ -355,6 +356,7 @@ public class LoanProductsApiResource {
Collection<ChargeToGLAccountMapper> feeToGLAccountMappings; Collection<ChargeToGLAccountMapper> feeToGLAccountMappings;
Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings; Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings;
List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings; List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings;
List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseAccountMappings;
List<ClassificationToGLAccountData> capitalizedIncomeClassificationToGLAccountMappings; List<ClassificationToGLAccountData> capitalizedIncomeClassificationToGLAccountMappings;
List<ClassificationToGLAccountData> buydowFeeClassificationToGLAccountMappings; List<ClassificationToGLAccountData> buydowFeeClassificationToGLAccountMappings;
if (loanProduct.hasAccountingEnabled()) { if (loanProduct.hasAccountingEnabled()) {
@@ -367,6 +369,8 @@ public class LoanProductsApiResource {
.fetchPenaltyToIncomeAccountMappingsForLoanProduct(productId); .fetchPenaltyToIncomeAccountMappingsForLoanProduct(productId);
chargeOffReasonToGLAccountMappings = this.accountMappingReadPlatformService chargeOffReasonToGLAccountMappings = this.accountMappingReadPlatformService
.fetchChargeOffReasonMappingsForLoanProduct(productId); .fetchChargeOffReasonMappingsForLoanProduct(productId);
writeOffReasonsToExpenseAccountMappings = this.accountMappingReadPlatformService
.fetchWriteOffReasonMappingsForLoanProduct(productId);
capitalizedIncomeClassificationToGLAccountMappings = accountMappingReadPlatformService capitalizedIncomeClassificationToGLAccountMappings = accountMappingReadPlatformService
.fetchClassificationMappingsForLoanProduct(productId, .fetchClassificationMappingsForLoanProduct(productId,
LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
@@ -374,7 +378,8 @@ public class LoanProductsApiResource {
productId, LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); productId, LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
loanProduct = LoanProductData.withAccountingDetails(loanProduct, accountingMappings, paymentChannelToFundSourceMappings, loanProduct = LoanProductData.withAccountingDetails(loanProduct, accountingMappings, paymentChannelToFundSourceMappings,
feeToGLAccountMappings, penaltyToGLAccountMappings, chargeOffReasonToGLAccountMappings, feeToGLAccountMappings, penaltyToGLAccountMappings, chargeOffReasonToGLAccountMappings,
capitalizedIncomeClassificationToGLAccountMappings, buydowFeeClassificationToGLAccountMappings); writeOffReasonsToExpenseAccountMappings, capitalizedIncomeClassificationToGLAccountMappings,
buydowFeeClassificationToGLAccountMappings);
} }
if (settings.isTemplate()) { if (settings.isTemplate()) {
@@ -475,6 +480,8 @@ public class LoanProductsApiResource {
.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class); .getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class);
final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions = ApiFacingEnum final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions = ApiFacingEnum
.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class); .getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class);
final List<CodeValueData> writeOffReasonOptions = codeValueReadPlatformService
.retrieveCodeValuesByCode(LoanApiConstants.WRITEOFFREASONS);
final List<CodeValueData> capitalizedIncomeClassificationOptions = codeValueReadPlatformService final List<CodeValueData> capitalizedIncomeClassificationOptions = codeValueReadPlatformService
.retrieveCodeValuesByCode(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE); .retrieveCodeValuesByCode(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE);
final List<CodeValueData> buydownFeeClassificationOptions = codeValueReadPlatformService final List<CodeValueData> buydownFeeClassificationOptions = codeValueReadPlatformService
@@ -493,7 +500,7 @@ public class LoanProductsApiResource {
creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions, creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions,
daysInYearCustomStrategyOptions, capitalizedIncomeCalculationTypeOptions, capitalizedIncomeStrategyOptions, daysInYearCustomStrategyOptions, capitalizedIncomeCalculationTypeOptions, capitalizedIncomeStrategyOptions,
capitalizedIncomeTypeOptions, buyDownFeeCalculationTypeOptions, buyDownFeeStrategyOptions, buyDownFeeIncomeTypeOptions, capitalizedIncomeTypeOptions, buyDownFeeCalculationTypeOptions, buyDownFeeStrategyOptions, buyDownFeeIncomeTypeOptions,
capitalizedIncomeClassificationOptions, buydownFeeClassificationOptions); writeOffReasonOptions, capitalizedIncomeClassificationOptions, buydownFeeClassificationOptions);
} }
} }
@@ -33,6 +33,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer;
import lombok.RequiredArgsConstructor; 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;
@@ -152,7 +153,9 @@ public final class LoanProductDataValidator {
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(),
LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(), LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(),
LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), LoanProductAccountingParams.WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(),
LoanProductAccountingParams.WRITE_OFF_REASON_CODE_VALUE_ID.getValue(),
LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(),
LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(),
LoanProductAccountingParams.INCOME_FROM_CAPITALIZATION.getValue(), LoanProductAccountingParams.INCOME_FROM_CAPITALIZATION.getValue(),
LoanProductAccountingParams.DEFERRED_INCOME_LIABILITY.getValue(), LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), LoanProductAccountingParams.DEFERRED_INCOME_LIABILITY.getValue(), LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(),
@@ -748,6 +751,7 @@ public final class LoanProductDataValidator {
validatePaymentChannelFundSourceMappings(baseDataValidator, element); validatePaymentChannelFundSourceMappings(baseDataValidator, element);
validateChargeToIncomeAccountMappings(baseDataValidator, element); validateChargeToIncomeAccountMappings(baseDataValidator, element);
validateChargeOffToExpenseMappings(baseDataValidator, element); validateChargeOffToExpenseMappings(baseDataValidator, element);
validateWriteOffToExpenseMappings(baseDataValidator, element);
validateClassificationToIncomeMappings(baseDataValidator, element, validateClassificationToIncomeMappings(baseDataValidator, element,
LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS); LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
validateClassificationToIncomeMappings(baseDataValidator, element, validateClassificationToIncomeMappings(baseDataValidator, element,
@@ -2068,54 +2072,79 @@ public final class LoanProductDataValidator {
private void validateChargeOffToExpenseMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) { private void validateChargeOffToExpenseMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
String parameterName = LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(); String parameterName = LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue();
LoanProductAccountingParams reasonCodeValueId = LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID;
String failCode = "chargeOffReason";
validateAdditionalAccountMappings(baseDataValidator, element, parameterName, reasonCodeValueId, failCode,
productToGLAccountMappingHelper::validateChargeOffMappingsInDatabase);
}
private void validateWriteOffToExpenseMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
String parameterName = LoanProductAccountingParams.WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue();
LoanProductAccountingParams reasonCodeValueId = LoanProductAccountingParams.WRITE_OFF_REASON_CODE_VALUE_ID;
String failCode = "writeOffReason";
validateAdditionalAccountMappings(baseDataValidator, element, parameterName, reasonCodeValueId, failCode,
productToGLAccountMappingHelper::validateWriteOffMappingsInDatabase);
}
private void validateAdditionalAccountMappings(DataValidatorBuilder baseDataValidator, JsonElement element, String parameterName,
LoanProductAccountingParams reasonCodeValueIdParam, String failCode,
BiConsumer<List<ApiParameterError>, List<JsonObject>> additionalMappingValidator) {
if (this.fromApiJsonHelper.parameterExists(parameterName, element)) { if (this.fromApiJsonHelper.parameterExists(parameterName, element)) {
final JsonArray chargeOffToExpenseMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element); final JsonArray reasonToExpenseMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element);
if (chargeOffToExpenseMappingArray != null && chargeOffToExpenseMappingArray.size() > 0) { if (reasonToExpenseMappingArray != null && !reasonToExpenseMappingArray.isEmpty()) {
Map<Long, Set<Long>> chargeOffReasonToAccounts = new HashMap<>(); Map<Long, Set<Long>> reasonToAccounts = new HashMap<>();
List<JsonObject> processedMappings = new ArrayList<>(); // Collect processed mappings for the new method List<JsonObject> processedMappings = new ArrayList<>(); // Collect processed mappings for the new method
int i = 0; int i = 0;
do { do {
final JsonObject jsonObject = chargeOffToExpenseMappingArray.get(i).getAsJsonObject(); final JsonObject jsonObject = reasonToExpenseMappingArray.get(i).getAsJsonObject();
final Long expenseGlAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject); final String expenseGlAccountIdString = this.fromApiJsonHelper
final Long chargeOffReasonCodeValueId = this.fromApiJsonHelper .extractStringNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject);
.extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), jsonObject); final String reasonCodeValueIdString = this.fromApiJsonHelper.extractStringNamed(reasonCodeValueIdParam.getValue(),
jsonObject);
// Validate parameters locally // Validate parameters locally
baseDataValidator.reset() baseDataValidator.reset()
.parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT .parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT
+ LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()) + LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue())
.value(expenseGlAccountId).notNull().integerGreaterThanZero(); .value(expenseGlAccountIdString).notNull().longGreaterThanZero();
baseDataValidator.reset() baseDataValidator.reset().parameter(
.parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT + reasonCodeValueIdParam.getValue())
+ LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue()) .value(reasonCodeValueIdString).notNull().longGreaterThanZero();
.value(chargeOffReasonCodeValueId).notNull().integerGreaterThanZero();
// Handle duplicate charge-off reason and GL Account validation final Long reasonCodeValueId = Long.valueOf(reasonCodeValueIdString);
chargeOffReasonToAccounts.putIfAbsent(chargeOffReasonCodeValueId, new HashSet<>()); final Long expenseGlAccountId = Long.valueOf(expenseGlAccountIdString);
Set<Long> associatedAccounts = chargeOffReasonToAccounts.get(chargeOffReasonCodeValueId); // Handle duplicate reason and GL Account validation
reasonToAccounts.putIfAbsent(reasonCodeValueId, new HashSet<>());
Set<Long> associatedAccounts = reasonToAccounts.get(reasonCodeValueId);
if (associatedAccounts.contains(expenseGlAccountId)) { if (associatedAccounts.contains(expenseGlAccountId)) {
baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET) baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET)
.failWithCode("duplicate.chargeOffReason.and.glAccount"); .failWithCode("duplicate." + failCode + ".and.glAccount");
} }
associatedAccounts.add(expenseGlAccountId); associatedAccounts.add(expenseGlAccountId);
if (associatedAccounts.size() > 1) { if (associatedAccounts.size() > 1) {
baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET) baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET)
.failWithCode("multiple.glAccounts.for.chargeOffReason"); .failWithCode("multiple.glAccounts.for." + failCode);
} }
// Collect mapping for additional validations // Collect mapping for additional validations
processedMappings.add(jsonObject); processedMappings.add(jsonObject);
i++; i++;
} while (i < chargeOffToExpenseMappingArray.size()); } while (i < reasonToExpenseMappingArray.size());
// Call the new validation method for additional checks // Call the new validation method for additional checks
productToGLAccountMappingHelper.validateChargeOffMappingsInDatabase(processedMappings); final List<ApiParameterError> validationErrors = new ArrayList<>();
productToGLAccountMappingHelper.validateGLAccountInDatabase(validationErrors, processedMappings);
if (additionalMappingValidator != null) {
additionalMappingValidator.accept(validationErrors, processedMappings);
}
if (!validationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(validationErrors);
}
} }
} }
} }
@@ -606,7 +606,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
loanChargeOffBehaviour.getValueAsStringEnumOptionData(), interestRecognitionOnDisbursementDate, loanChargeOffBehaviour.getValueAsStringEnumOptionData(), interestRecognitionOnDisbursementDate,
daysInYearCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy, daysInYearCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy,
capitalizedIncome, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, capitalizedIncome, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType,
merchantBuyDownFee); merchantBuyDownFee, null, null);
} }
} }
@@ -217,4 +217,5 @@
<include file="parts/0196_add_deleted_and_closed_to_buy_down_fee_balance.xml" relativeToChangelogFile="true" /> <include file="parts/0196_add_deleted_and_closed_to_buy_down_fee_balance.xml" relativeToChangelogFile="true" />
<include file="parts/0197_add_deleted_and_closed_to_capitalized_income_balance.xml" relativeToChangelogFile="true" /> <include file="parts/0197_add_deleted_and_closed_to_capitalized_income_balance.xml" relativeToChangelogFile="true" />
<include file="parts/0198_add_classification_id_to_acc_product_mapping.xml" relativeToChangelogFile="true" /> <include file="parts/0198_add_classification_id_to_acc_product_mapping.xml" relativeToChangelogFile="true" />
<include file="parts/0199_write_off_reason_mapping_loan.xml" relativeToChangelogFile="true" />
</databaseChangeLog> </databaseChangeLog>
@@ -0,0 +1,34 @@
<?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="17588202536-1" author="fineract">
<addColumn tableName="acc_product_mapping">
<column name="write_off_reason_id" type="BIGINT">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -85,6 +85,7 @@ import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.models.PostRolesRequest; import org.apache.fineract.client.models.PostRolesRequest;
import org.apache.fineract.client.models.PostUsersRequest; import org.apache.fineract.client.models.PostUsersRequest;
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest; import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
import org.apache.fineract.client.models.PutLoansApprovedAmountRequest; import org.apache.fineract.client.models.PutLoansApprovedAmountRequest;
import org.apache.fineract.client.models.PutLoansApprovedAmountResponse; import org.apache.fineract.client.models.PutLoansApprovedAmountResponse;
import org.apache.fineract.client.models.PutLoansAvailableDisbursementAmountRequest; import org.apache.fineract.client.models.PutLoansAvailableDisbursementAmountRequest;
@@ -507,6 +508,103 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
.loanScheduleType(LoanScheduleType.CUMULATIVE.toString());// .loanScheduleType(LoanScheduleType.CUMULATIVE.toString());//
} }
protected PutLoanProductsProductIdRequest update4IProgressive(String name, String shortName, Long delinquencyBucketId) {
return new PutLoanProductsProductIdRequest().name(name).shortName(shortName).description("4 installment product - progressive")//
.includeInBorrowerCycle(false)//
.useBorrowerCycle(false)//
.currencyCode("EUR")//
.digitsAfterDecimal(2)//
.principal(1000.0)//
.minPrincipal(100.0)//
.maxPrincipal(10000.0)//
.numberOfRepayments(4)//
.repaymentEvery(1)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L.intValue())//
.interestRatePerPeriod(10D)//
.minInterestRatePerPeriod(0D)//
.maxInterestRatePerPeriod(120D)//
.interestRateFrequencyType(InterestRateFrequencyType.YEARS)//
.isLinkedToFloatingInterestRates(false)//
.isLinkedToFloatingInterestRates(false)//
.allowVariableInstallments(false)//
.amortizationType(AmortizationType.EQUAL_INSTALLMENTS)//
.interestType(InterestType.DECLINING_BALANCE)//
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
.allowPartialPeriodInterestCalcualtion(false)//
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
.paymentAllocation(List.of(createDefaultPaymentAllocation("NEXT_INSTALLMENT")))//
.creditAllocation(List.of())//
.overdueDaysForNPA(179)//
.daysInMonthType(30L)//
.daysInYearType(360L)//
.isInterestRecalculationEnabled(true)//
.interestRecalculationCompoundingMethod(0)//
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
.recalculationRestFrequencyInterval(1)//
.isArrearsBasedOnOriginalSchedule(false)//
.isCompoundingToBePostedAsTransaction(false)//
.preClosureInterestCalculationStrategy(1)//
.allowCompoundingOnEod(false)//
.canDefineInstallmentAmount(true)//
.repaymentStartDateType(1)//
.charges(List.of())//
.principalVariationsForBorrowerCycle(List.of())//
.interestRateVariationsForBorrowerCycle(List.of())//
.numberOfRepaymentVariationsForBorrowerCycle(List.of())//
.accountingRule(3)//
.canUseForTopup(false)//
.fundSourceAccountId(fundSource.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())//
.incomeFromChargeOffPenaltyAccountId(penaltyChargeOffAccount.getAccountID().longValue())//
.chargeOffExpenseAccountId(chargeOffExpenseAccount.getAccountID().longValue())//
.chargeOffFraudExpenseAccountId(chargeOffFraudExpenseAccount.getAccountID().longValue())//
.dateFormat(DATETIME_PATTERN)//
.locale("en")//
.enableAccrualActivityPosting(false)//
.multiDisburseLoan(true)//
.maxTrancheCount(10)//
.outstandingLoanBalance(10000.0)//
.disallowExpectedDisbursements(true)//
.allowApprovedDisbursedAmountsOverApplied(true)//
.overAppliedCalculationType("percentage")//
.overAppliedNumber(50)//
.principalThresholdForLastInstallment(50)//
.holdGuaranteeFunds(false)//
.accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
.allowAttributeOverrides(new AllowAttributeOverrides()//
.amortizationType(true)//
.interestType(true)//
.transactionProcessingStrategyCode(true)//
.interestCalculationPeriodType(true)//
.inArrearsTolerance(true)//
.repaymentEvery(true)//
.graceOnPrincipalAndInterestPayment(true)//
.graceOnArrearsAgeing(true)//
).isEqualAmortization(false)//
.delinquencyBucketId(delinquencyBucketId)//
.enableDownPayment(false)//
.enableInstallmentLevelDelinquency(false)//
.loanScheduleType("PROGRESSIVE")//
.loanScheduleProcessingType("HORIZONTAL");
}
protected PostLoanProductsRequest create4IProgressive() { protected PostLoanProductsRequest create4IProgressive() {
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
Assertions.assertNotNull(delinquencyBucketId); Assertions.assertNotNull(delinquencyBucketId);
@@ -19,13 +19,23 @@
package org.apache.fineract.integrationtests; package org.apache.fineract.integrationtests;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoanProductsTemplateResponse;
import org.apache.fineract.client.models.GetLoanProductsWriteOffReasonOptions;
import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostCodeValueDataResponse;
import org.apache.fineract.client.models.PostCodeValuesDataRequest;
import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostWriteOffReasonToExpenseAccountMappings;
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest; import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.FineractClientHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeCalculationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeCalculationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy;
@@ -36,6 +46,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@Slf4j
public class LoanProductTest extends BaseLoanIntegrationTest { public class LoanProductTest extends BaseLoanIntegrationTest {
@Nested @Nested
@@ -458,4 +469,118 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue()))); .buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())));
} }
} }
@Nested
public class WriteOffReasonsToExpenseMappings {
@Test
public void testWriteOffReasonToExpenseAccountMapping_shouldFail_on_nonExistingGLAccount_And_nonExistingWriteOffReason() {
try {
loanProductHelper.createLoanProduct(
create4IProgressive().addWriteOffReasonsToExpenseMappingsItem(new PostWriteOffReasonToExpenseAccountMappings()
.expenseAccountId("101230023").writeOffReasonCodeValueId("201230023")));
Assertions.fail("Should have thrown an IllegalArgumentException");
} catch (final RuntimeException ex) {
Assertions.assertTrue(
ex.getMessage().contains("GL Account with ID 101230023 does not exist or is not an Expense GL account"));
Assertions.assertTrue(ex.getMessage().contains("Write-off reason with ID 201230023 does not exist"));
}
}
@Test
public void testWriteOffReasonToExpenseAccountMapping_shouldFail_on_nonExistingGLAccount_And_Invalid_expenseAccountId() {
try {
loanProductHelper.createLoanProduct(create4IProgressive().addWriteOffReasonsToExpenseMappingsItem(
new PostWriteOffReasonToExpenseAccountMappings().expenseAccountId("asdf323").writeOffReasonCodeValueId("111")));
Assertions.fail("Should have thrown an IllegalArgumentException");
} catch (final RuntimeException ex) {
Assertions.assertTrue(ex.getMessage()
.contains("validation.msg.loanproduct.writeOffReasonsToExpenseMappings[0].expenseAccountId.not.a.number"));
Assertions.assertTrue(
ex.getMessage().contains("The parameter `writeOffReasonsToExpenseMappings[0].expenseAccountId` must be a number."));
}
}
@Test
public void testWriteOffReasonToExpenseAccountMapping_shouldFail_on_nonExistingGLAccount_And_Invalid_writeOffReasonCodeValueId() {
try {
loanProductHelper.createLoanProduct(create4IProgressive().addWriteOffReasonsToExpenseMappingsItem(
new PostWriteOffReasonToExpenseAccountMappings().expenseAccountId("111").writeOffReasonCodeValueId("asdf323")));
Assertions.fail("Should have thrown an IllegalArgumentException");
} catch (final RuntimeException ex) {
log.info("Exception: {}", ex.getMessage());
Assertions.assertTrue(ex.getMessage()
.contains("validation.msg.loanproduct.writeOffReasonsToExpenseMappings[0].writeOffReasonCodeValueId.not.a.number"));
Assertions.assertTrue(ex.getMessage()
.contains("The parameter `writeOffReasonsToExpenseMappings[0].writeOffReasonCodeValueId` must be a number."));
}
}
@Test
public void testWriteOffReasonsToExpenseMappings() {
// create Write Off reasons
Long reasonCode1 = createTestWriteOffReason();
Long reasonCode2 = createTestWriteOffReason();
// check if write Off reasons appears on loan product template
GetLoanProductsTemplateResponse loanProductTemplate = loanProductHelper.getLoanProductTemplate(false);
List<GetLoanProductsWriteOffReasonOptions> writeOffReasonOptions = loanProductTemplate.getWriteOffReasonOptions();
Assertions.assertNotNull(writeOffReasonOptions);
boolean isReasonCode1InTemplate = writeOffReasonOptions.stream().map(GetLoanProductsWriteOffReasonOptions::getId)
.anyMatch(id -> Objects.equals(id, reasonCode1));
boolean isReasonCode2InTemplate = writeOffReasonOptions.stream().map(GetLoanProductsWriteOffReasonOptions::getId)
.anyMatch(id -> Objects.equals(id, reasonCode2));
Assertions.assertTrue(isReasonCode1InTemplate);
Assertions.assertTrue(isReasonCode2InTemplate);
// Create Test Loan Product
String reasonCodeId = reasonCode1.toString();
String expenseAccountId = buyDownExpenseAccount.getAccountID().toString();
Long loanProductId = loanProductHelper.createLoanProduct(
create4IProgressive().addWriteOffReasonsToExpenseMappingsItem(new PostWriteOffReasonToExpenseAccountMappings()
.expenseAccountId(expenseAccountId).writeOffReasonCodeValueId(reasonCodeId)))
.getResourceId();
// Verify that get loan product API has the corresponding fields
GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = loanProductHelper.retrieveLoanProductById(loanProductId);
List<PostWriteOffReasonToExpenseAccountMappings> writeOffReasonToExpenseAccountMappings = getLoanProductsProductIdResponse
.getWriteOffReasonsToExpenseMappings();
Assertions.assertNotNull(writeOffReasonToExpenseAccountMappings);
Assertions.assertEquals(1, writeOffReasonToExpenseAccountMappings.size());
PostWriteOffReasonToExpenseAccountMappings writeOffMapping = writeOffReasonToExpenseAccountMappings.getFirst();
Assertions.assertNotNull(writeOffMapping);
Assertions.assertEquals(expenseAccountId, writeOffMapping.getExpenseAccountId());
Assertions.assertEquals(reasonCodeId, writeOffMapping.getWriteOffReasonCodeValueId());
List<GetLoanProductsWriteOffReasonOptions> writeOffReasonOptionsResultNonTemplate = getLoanProductsProductIdResponse
.getWriteOffReasonOptions();
if (writeOffReasonOptionsResultNonTemplate != null && !writeOffReasonOptionsResultNonTemplate.isEmpty()) {
Assertions.fail("Write-off reason options with no template setting should be empty");
}
// test Update loan product API - delete writeOffReasonsToExpenseMappings
GetLoanProductsProductIdResponse getLoanProductsProductId = loanProductHelper.retrieveLoanProductById(loanProductId);
loanProductHelper.updateLoanProductById(loanProductId,
update4IProgressive(getLoanProductsProductId.getName(), getLoanProductsProductId.getShortName(),
getLoanProductsProductId.getDelinquencyBucket().getId()).writeOffReasonsToExpenseMappings(List.of()));
// Verify that get loan product API has the corresponding fields
Assertions.assertNull(loanProductHelper.retrieveLoanProductById(loanProductId).getWriteOffReasonsToExpenseMappings());
}
}
private Long createTestWriteOffReason() {
PostCodeValueDataResponse response = okR(FineractClientHelper.getFineractClient().codeValues.createCodeValue(26L,
new PostCodeValuesDataRequest().name(Utils.uniqueRandomStringGenerator("TestWriteOffReason_1_", 6))
.description("Test write off reason value 1").isActive(true).position(0)))
.body();
Assertions.assertNotNull(response);
Assertions.assertNotNull(response.getSubResourceId());
return response.getSubResourceId();
}
} }
@@ -834,7 +834,7 @@ public class LoanProductTestBuilder {
} }
Map<String, Long> newMap = new HashMap<>(); Map<String, Long> newMap = new HashMap<>();
newMap.put("chargeOffReasonCodeValueId", reasonId); newMap.put("chargeOffReasonCodeValueId", reasonId);
newMap.put("expenseGLAccountId", accountId); newMap.put("expenseAccountId", accountId);
this.chargeOffReasonToExpenseAccountMappings.add(newMap); this.chargeOffReasonToExpenseAccountMappings.add(newMap);
return this; return this;
} }