FINERACT-2358: Allow to configure advanced accounting rules based on write-off reason
This commit is contained in:
+4
@@ -68,6 +68,10 @@ public class ProductToGLAccountMapping extends AbstractPersistableCustom<Long> {
|
||||
@JoinColumn(name = "charge_off_reason_id", nullable = true)
|
||||
private CodeValue chargeOffReason;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "write_off_reason_id", nullable = true)
|
||||
private CodeValue writeOffReason;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "capitalized_income_classification_id", nullable = true)
|
||||
private CodeValue capitalizedIncomeClassification;
|
||||
|
||||
+9
-2
@@ -35,7 +35,7 @@ public interface ProductToGLAccountMappingRepository
|
||||
@Param("productType") int productType, @Param("financialAccountType") int financialAccountType,
|
||||
@Param("chargeId") Long ChargeId);
|
||||
|
||||
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReason is NULL 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,
|
||||
@Param("financialAccountType") int financialAccountType);
|
||||
|
||||
@@ -66,11 +66,18 @@ public interface ProductToGLAccountMappingRepository
|
||||
List<ProductToGLAccountMapping> findAllChargeOffReasonsMappings(@Param("productId") Long productId,
|
||||
@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")
|
||||
ProductToGLAccountMapping findChargeOffReasonMapping(@Param("productId") Long productId, @Param("productType") Integer productType,
|
||||
@Param("chargeOffReasonId") Long chargeOffReasonId);
|
||||
|
||||
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL 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);
|
||||
|
||||
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.paymentType is not NULL")
|
||||
|
||||
+113
-61
@@ -29,6 +29,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan;
|
||||
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,
|
||||
final Map<String, Object> changes, final PortfolioProductType portfolioProductType) {
|
||||
public void saveReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
|
||||
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 JsonArray chargeOffReasonToExpenseAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayName, element);
|
||||
final String arrayName = arrayNameParam.getValue();
|
||||
final JsonArray reasonToExpenseAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayName, element);
|
||||
|
||||
if (chargeOffReasonToExpenseAccountMappingArray != null) {
|
||||
if (reasonToExpenseAccountMappingArray != null) {
|
||||
if (changes != null) {
|
||||
changes.put(arrayName, command.jsonFragment(arrayName));
|
||||
}
|
||||
|
||||
for (int i = 0; i < chargeOffReasonToExpenseAccountMappingArray.size(); i++) {
|
||||
final JsonObject jsonObject = chargeOffReasonToExpenseAccountMappingArray.get(i).getAsJsonObject();
|
||||
final Long reasonId = jsonObject.get(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue()).getAsLong();
|
||||
for (int i = 0; i < reasonToExpenseAccountMappingArray.size(); i++) {
|
||||
final JsonObject jsonObject = reasonToExpenseAccountMappingArray.get(i).getAsJsonObject();
|
||||
final Long reasonId = jsonObject.get(reasonCodeValueIdParam.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,
|
||||
final Map<String, Object> changes, final PortfolioProductType portfolioProductType) {
|
||||
private Long getReasonIdByCashAccountForLoan(final ProductToGLAccountMapping productToGLAccountMapping,
|
||||
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
|
||||
.findAllChargeOffReasonsMappings(productId, portfolioProductType.getValue());
|
||||
final JsonArray chargeOffReasonToGLAccountMappingArray = this.fromApiJsonHelper
|
||||
.extractJsonArrayNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(), element);
|
||||
public void updateReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
|
||||
final Map<String, Object> changes, final PortfolioProductType portfolioProductType,
|
||||
final List<ProductToGLAccountMapping> existingReasonToGLAccountMappings,
|
||||
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<>();
|
||||
if (chargeOffReasonToGLAccountMappingArray != null) {
|
||||
final Map<Long, Long> inputReasonToGLAccountMap = new HashMap<>();
|
||||
|
||||
final Set<Long> existingReasons = new HashSet<>();
|
||||
if (reasonToGLAccountMappingArray != null) {
|
||||
if (changes != null) {
|
||||
changes.put(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(),
|
||||
command.jsonFragment(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue()));
|
||||
changes.put(reasonToExpenseAccountMappingsParam.getValue(),
|
||||
command.jsonFragment(reasonToExpenseAccountMappingsParam.getValue()));
|
||||
}
|
||||
|
||||
for (int i = 0; i < chargeOffReasonToGLAccountMappingArray.size(); i++) {
|
||||
final JsonObject jsonObject = chargeOffReasonToGLAccountMappingArray.get(i).getAsJsonObject();
|
||||
for (int i = 0; i < reasonToGLAccountMappingArray.size(); i++) {
|
||||
final JsonObject jsonObject = reasonToGLAccountMappingArray.get(i).getAsJsonObject();
|
||||
final Long expenseGlAccountId = jsonObject.get(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()).getAsLong();
|
||||
final Long chargeOffReasonCodeValueId = jsonObject
|
||||
.get(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue()).getAsLong();
|
||||
inputChargeOffReasonToGLAccountMap.put(chargeOffReasonCodeValueId, expenseGlAccountId);
|
||||
final Long reasonCodeValueId = jsonObject.get(reasonCodeValueIdParam.getValue()).getAsLong();
|
||||
inputReasonToGLAccountMap.put(reasonCodeValueId, expenseGlAccountId);
|
||||
}
|
||||
|
||||
// If input map is empty, delete all existing mappings
|
||||
if (inputChargeOffReasonToGLAccountMap.isEmpty()) {
|
||||
this.accountMappingRepository.deleteAll(existingChargeOffReasonToGLAccountMappings);
|
||||
if (inputReasonToGLAccountMap.isEmpty()) {
|
||||
this.accountMappingRepository.deleteAll(existingReasonToGLAccountMappings);
|
||||
|
||||
} else {
|
||||
for (final ProductToGLAccountMapping existingChargeOffReasonToGLAccountMapping : existingChargeOffReasonToGLAccountMappings) {
|
||||
final Long currentChargeOffReasonId = existingChargeOffReasonToGLAccountMapping.getChargeOffReason().getId();
|
||||
if (currentChargeOffReasonId != null) {
|
||||
existingChargeOffReasons.add(currentChargeOffReasonId);
|
||||
for (final ProductToGLAccountMapping existingReasonToGLAccountMapping : existingReasonToGLAccountMappings) {
|
||||
final Long currentReasonId = getReasonIdByCashAccountForLoan(existingReasonToGLAccountMapping, cashAccountsForLoan);
|
||||
if (currentReasonId != null) {
|
||||
existingReasons.add(currentReasonId);
|
||||
// update existing mappings (if required)
|
||||
if (inputChargeOffReasonToGLAccountMap.containsKey(currentChargeOffReasonId)) {
|
||||
final Long newGLAccountId = inputChargeOffReasonToGLAccountMap.get(currentChargeOffReasonId);
|
||||
if (!newGLAccountId.equals(existingChargeOffReasonToGLAccountMapping.getGlAccount().getId())) {
|
||||
if (inputReasonToGLAccountMap.containsKey(currentReasonId)) {
|
||||
final Long newGLAccountId = inputReasonToGLAccountMap.get(currentReasonId);
|
||||
if (!newGLAccountId.equals(existingReasonToGLAccountMapping.getGlAccount().getId())) {
|
||||
final Optional<GLAccount> glAccount = accountRepository.findById(newGLAccountId);
|
||||
if (glAccount.isPresent()) {
|
||||
existingChargeOffReasonToGLAccountMapping.setGlAccount(glAccount.get());
|
||||
this.accountMappingRepository.saveAndFlush(existingChargeOffReasonToGLAccountMapping);
|
||||
existingReasonToGLAccountMapping.setGlAccount(glAccount.get());
|
||||
this.accountMappingRepository.saveAndFlush(existingReasonToGLAccountMapping);
|
||||
}
|
||||
}
|
||||
} // deleted payment type
|
||||
else {
|
||||
this.accountMappingRepository.delete(existingChargeOffReasonToGLAccountMapping);
|
||||
this.accountMappingRepository.delete(existingReasonToGLAccountMapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only the newly added
|
||||
for (Map.Entry<Long, Long> entry : inputChargeOffReasonToGLAccountMap.entrySet().stream()
|
||||
.filter(e -> !existingChargeOffReasons.contains(e.getKey())).toList()) {
|
||||
saveChargeOffReasonToExpenseMapping(productId, entry.getKey(), entry.getValue(), portfolioProductType);
|
||||
for (Map.Entry<Long, Long> entry : inputReasonToGLAccountMap.entrySet().stream()
|
||||
.filter(e -> !existingReasons.contains(e.getKey())).toList()) {
|
||||
saveReasonToExpenseMapping(productId, entry.getKey(), entry.getValue(), portfolioProductType, cashAccountsForLoan);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -587,21 +604,37 @@ public class ProductToGLAccountMappingHelper {
|
||||
this.accountMappingRepository.saveAndFlush(accountMapping);
|
||||
}
|
||||
|
||||
private void saveChargeOffReasonToExpenseMapping(final Long productId, final Long reasonId, final Long expenseAccountId,
|
||||
final PortfolioProductType portfolioProductType) {
|
||||
private Predicate<? super ProductToGLAccountMapping> matching(final CashAccountsForLoan typeDef, final Long reasonId) {
|
||||
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 boolean reasonMappingExists = this.accountMappingRepository
|
||||
.findAllChargeOffReasonsMappings(productId, portfolioProductType.getValue()).stream()
|
||||
.anyMatch(mapping -> mapping.getChargeOffReason().getId().equals(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())
|
||||
.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);
|
||||
}
|
||||
@@ -703,22 +736,25 @@ public class ProductToGLAccountMappingHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public void validateChargeOffMappingsInDatabase(final List<JsonObject> mappings) {
|
||||
final List<ApiParameterError> validationErrors = new ArrayList<>();
|
||||
public void validateWriteOffMappingsInDatabase(final List<ApiParameterError> validationErrors, final List<JsonObject> mappings) {
|
||||
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) {
|
||||
final Long expenseGlAccountId = this.fromApiJsonHelper
|
||||
.extractLongNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject);
|
||||
final Long chargeOffReasonCodeValueId = this.fromApiJsonHelper
|
||||
.extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), jsonObject);
|
||||
|
||||
// Validation: chargeOffReasonCodeValueId must exist in the database
|
||||
CodeValue codeValue = this.codeValueRepository.findByCodeNameAndId("ChargeOffReasons", chargeOffReasonCodeValueId);
|
||||
if (codeValue == null) {
|
||||
validationErrors.add(ApiParameterError.parameterError("validation.msg.chargeoffreason.invalid",
|
||||
"Charge-off reason with ID " + chargeOffReasonCodeValueId + " does not exist",
|
||||
LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue()));
|
||||
}
|
||||
|
||||
// Validation: expenseGLAccountId must exist as a valid Expense GL account
|
||||
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
|
||||
if (!validationErrors.isEmpty()) {
|
||||
|
||||
+3
@@ -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.ClassificationToGLAccountData;
|
||||
import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
|
||||
import org.apache.fineract.accounting.producttoaccountmapping.data.WriteOffReasonsToExpenseAccountMapper;
|
||||
|
||||
public interface ProductToGLAccountMappingReadPlatformService {
|
||||
|
||||
@@ -52,6 +53,8 @@ public interface ProductToGLAccountMappingReadPlatformService {
|
||||
|
||||
List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId);
|
||||
|
||||
List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappingsForLoanProduct(Long loanProductId);
|
||||
|
||||
List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId,
|
||||
LoanProductAccountingParams classificationParameter);
|
||||
}
|
||||
|
||||
+22
@@ -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.ClassificationToGLAccountData;
|
||||
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.ProductToGLAccountMappingRepository;
|
||||
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
|
||||
@@ -293,6 +294,22 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
|
||||
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,
|
||||
final Long loanProductId, LoanProductAccountingParams classificationParameter) {
|
||||
final List<ProductToGLAccountMapping> mappings = classificationParameter
|
||||
@@ -367,6 +384,11 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
|
||||
return fetchChargeOffReasonMappings(PortfolioProductType.LOAN, loanProductId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappingsForLoanProduct(Long loanProductId) {
|
||||
return fetchWriteOffReasonMappings(PortfolioProductType.LOAN, loanProductId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId,
|
||||
LoanProductAccountingParams classificationParameter) {
|
||||
|
||||
+2
@@ -181,8 +181,10 @@ public final class AccountingConstants {
|
||||
INCOME_FROM_GOODWILL_CREDIT_FEES("incomeFromGoodwillCreditFeesAccountId"), //
|
||||
INCOME_FROM_GOODWILL_CREDIT_PENALTY("incomeFromGoodwillCreditPenaltyAccountId"), //
|
||||
CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS("chargeOffReasonToExpenseAccountMappings"), //
|
||||
WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS("writeOffReasonsToExpenseMappings"), //
|
||||
EXPENSE_GL_ACCOUNT_ID("expenseAccountId"), //
|
||||
CHARGE_OFF_REASON_CODE_VALUE_ID("chargeOffReasonCodeValueId"), //
|
||||
WRITE_OFF_REASON_CODE_VALUE_ID("writeOffReasonCodeValueId"), //
|
||||
DEFERRED_INCOME_LIABILITY("deferredIncomeLiabilityAccountId"), //
|
||||
INCOME_FROM_CAPITALIZATION("incomeFromCapitalizationAccountId"), //
|
||||
BUY_DOWN_EXPENSE("buyDownExpenseAccountId"), //
|
||||
|
||||
+36
@@ -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;
|
||||
}
|
||||
+14
-7
@@ -531,13 +531,20 @@ public class DataValidatorBuilder {
|
||||
}
|
||||
|
||||
if (this.value != null) {
|
||||
final long number = Long.parseLong(this.value.toString());
|
||||
if (number < 1) {
|
||||
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".not.greater.than.zero";
|
||||
String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be greater than 0.";
|
||||
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage, this.parameter,
|
||||
number, 0);
|
||||
this.dataValidationErrors.add(error);
|
||||
try {
|
||||
final long number = Long.parseLong(this.value.toString());
|
||||
if (number < 1) {
|
||||
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".not.greater.than.zero";
|
||||
String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be greater than 0.";
|
||||
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage,
|
||||
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;
|
||||
|
||||
+30
-2
@@ -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.GLAccountRepositoryWrapper;
|
||||
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.exception.ProductToGLAccountMappingInvalidException;
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
|
||||
+48
-1
@@ -301,6 +301,7 @@ public final class LoanProductsApiResourceSwagger {
|
||||
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
||||
public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings;
|
||||
public List<PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings;
|
||||
public List<PostWriteOffReasonToExpenseAccountMappings> writeOffReasonsToExpenseMappings;
|
||||
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> buydownfeeClassificationToIncomeAccountMappings;
|
||||
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings;
|
||||
public List<LoanProductChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
||||
@@ -372,7 +373,7 @@ public final class LoanProductsApiResourceSwagger {
|
||||
@Schema(example = "REGULAR")
|
||||
public String chargeOffBehaviour;
|
||||
|
||||
static final class PostChargeOffReasonToExpenseAccountMappings {
|
||||
public static final class PostChargeOffReasonToExpenseAccountMappings {
|
||||
|
||||
private PostChargeOffReasonToExpenseAccountMappings() {}
|
||||
|
||||
@@ -382,6 +383,17 @@ public final class LoanProductsApiResourceSwagger {
|
||||
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 {
|
||||
|
||||
private PostClassificationToIncomeAccountMappings() {}
|
||||
@@ -1154,6 +1166,7 @@ public final class LoanProductsApiResourceSwagger {
|
||||
public List<StringEnumOptionData> supportedInterestRefundTypes;
|
||||
public List<StringEnumOptionData> supportedInterestRefundTypesOptions;
|
||||
public List<GetLoanProductsChargeOffReasonOptions> chargeOffReasonOptions;
|
||||
public List<GetLoanProductsWriteOffReasonOptions> writeOffReasonOptions;
|
||||
public StringEnumOptionData chargeOffBehaviour;
|
||||
public List<StringEnumOptionData> chargeOffBehaviourOptions;
|
||||
@Schema(example = "false")
|
||||
@@ -1380,6 +1393,17 @@ public final class LoanProductsApiResourceSwagger {
|
||||
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")
|
||||
public Long id;
|
||||
@Schema(example = "advanced accounting")
|
||||
@@ -1468,6 +1492,7 @@ public final class LoanProductsApiResourceSwagger {
|
||||
public Set<GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
||||
public Set<GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
|
||||
public List<GetChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings;
|
||||
public List<PostLoanProductsRequest.PostWriteOffReasonToExpenseAccountMappings> writeOffReasonsToExpenseMappings;
|
||||
@Schema(example = "false")
|
||||
public Boolean isRatesEnabled;
|
||||
@Schema(example = "true")
|
||||
@@ -1511,6 +1536,7 @@ public final class LoanProductsApiResourceSwagger {
|
||||
public Boolean enableAccrualActivityPosting;
|
||||
public List<StringEnumOptionData> supportedInterestRefundTypes;
|
||||
public List<GetLoanProductsChargeOffReasonOptions> chargeOffReasonOptions;
|
||||
public List<GetLoanProductsWriteOffReasonOptions> writeOffReasonOptions;
|
||||
public StringEnumOptionData chargeOffBehaviour;
|
||||
@Schema(example = "false")
|
||||
public Boolean interestRecognitionOnDisbursementDate;
|
||||
@@ -1774,6 +1800,7 @@ public final class LoanProductsApiResourceSwagger {
|
||||
public List<GetLoanProductsProductIdResponse.GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
|
||||
public List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings;
|
||||
public List<PostLoanProductsRequest.PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings;
|
||||
public List<PostLoanProductsRequest.PostWriteOffReasonToExpenseAccountMappings> writeOffReasonsToExpenseMappings;
|
||||
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> buydownfeeClassificationToIncomeAccountMappings;
|
||||
public List<PostLoanProductsRequest.PostClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings;
|
||||
public List<LoanProductChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
||||
@@ -1904,4 +1931,24 @@ public final class LoanProductsApiResourceSwagger {
|
||||
@Schema(example = "false")
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+31
-8
@@ -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.ClassificationToGLAccountData;
|
||||
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.core.api.ApiFacingEnum;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
@@ -165,7 +166,8 @@ public class LoanProductData implements Serializable {
|
||||
private Collection<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
|
||||
private List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToExpenseAccountMappings;
|
||||
private final boolean enableAccrualActivityPosting;
|
||||
|
||||
private List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings;
|
||||
private final List<CodeValueData> writeOffReasonOptions;
|
||||
// rates
|
||||
private final boolean isRatesEnabled;
|
||||
private final Collection<RateData> rates;
|
||||
@@ -379,6 +381,8 @@ public class LoanProductData implements Serializable {
|
||||
final StringEnumOptionData buyDownFeeStrategy = null;
|
||||
final StringEnumOptionData buyDownFeeIncomeType = null;
|
||||
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,
|
||||
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
|
||||
@@ -402,7 +406,8 @@ public class LoanProductData implements Serializable {
|
||||
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
|
||||
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
|
||||
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 buyDownFeeIncomeType = null;
|
||||
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,
|
||||
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
|
||||
@@ -539,7 +546,8 @@ public class LoanProductData implements Serializable {
|
||||
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
|
||||
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
|
||||
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 buyDownFeeIncomeType = null;
|
||||
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,
|
||||
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
|
||||
@@ -683,7 +693,8 @@ public class LoanProductData implements Serializable {
|
||||
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
|
||||
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
|
||||
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 buyDownFeeIncomeType = null;
|
||||
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,
|
||||
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
|
||||
@@ -821,7 +834,8 @@ public class LoanProductData implements Serializable {
|
||||
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
|
||||
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
|
||||
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,
|
||||
@@ -829,6 +843,7 @@ public class LoanProductData implements Serializable {
|
||||
final Collection<ChargeToGLAccountMapper> feeToGLAccountMappings,
|
||||
final Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings,
|
||||
final List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings,
|
||||
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonToGLAccountMappings,
|
||||
final List<ClassificationToGLAccountData> capitalizedIncomeClassificationToIncomeAccountMappings,
|
||||
final List<ClassificationToGLAccountData> buydownFeeClassificationToIncomeAccountMappings) {
|
||||
productData.accountingMappings = accountingMappings;
|
||||
@@ -836,6 +851,7 @@ public class LoanProductData implements Serializable {
|
||||
productData.feeToIncomeAccountMappings = feeToGLAccountMappings;
|
||||
productData.penaltyToIncomeAccountMappings = penaltyToGLAccountMappings;
|
||||
productData.chargeOffReasonToExpenseAccountMappings = chargeOffReasonToGLAccountMappings;
|
||||
productData.writeOffReasonsToExpenseMappings = writeOffReasonToGLAccountMappings;
|
||||
productData.capitalizedIncomeClassificationToIncomeAccountMappings = capitalizedIncomeClassificationToIncomeAccountMappings;
|
||||
productData.buydownFeeClassificationToIncomeAccountMappings = buydownFeeClassificationToIncomeAccountMappings;
|
||||
return productData;
|
||||
@@ -884,7 +900,9 @@ public class LoanProductData implements Serializable {
|
||||
final StringEnumOptionData capitalizedIncomeCalculationType, final StringEnumOptionData capitalizedIncomeStrategy,
|
||||
final StringEnumOptionData capitalizedIncomeType, final boolean enableBuyDownFee,
|
||||
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.name = name;
|
||||
this.shortName = shortName;
|
||||
@@ -971,6 +989,7 @@ public class LoanProductData implements Serializable {
|
||||
this.feeToIncomeAccountMappings = null;
|
||||
this.penaltyToIncomeAccountMappings = null;
|
||||
this.chargeOffReasonToExpenseAccountMappings = null;
|
||||
this.writeOffReasonsToExpenseMappings = null;
|
||||
this.valueConditionTypeOptions = null;
|
||||
this.principalVariationsForBorrowerCycle = principalVariations;
|
||||
this.interestRateVariationsForBorrowerCycle = interestRateVariations;
|
||||
@@ -1046,6 +1065,8 @@ public class LoanProductData implements Serializable {
|
||||
this.buyDownFeeCalculationTypeOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeCalculationType.class);
|
||||
this.buyDownFeeStrategyOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class);
|
||||
this.buyDownFeeIncomeTypeOptions = ApiFacingEnum.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class);
|
||||
this.writeOffReasonsToExpenseMappings = writeOffReasonsToExpenseMappings;
|
||||
this.writeOffReasonOptions = writeOffReasonOptions;
|
||||
this.capitalizedIncomeClassificationOptions = null;
|
||||
this.buydownFeeClassificationOptions = null;
|
||||
this.capitalizedIncomeClassificationToIncomeAccountMappings = null;
|
||||
@@ -1079,8 +1100,8 @@ public class LoanProductData implements Serializable {
|
||||
final List<StringEnumOptionData> capitalizedIncomeStrategyOptions,
|
||||
final List<StringEnumOptionData> capitalizedIncomeTypeOptions,
|
||||
final List<StringEnumOptionData> buyDownFeeCalculationTypeOptions, final List<StringEnumOptionData> buyDownFeeStrategyOptions,
|
||||
final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions, final List<CodeValueData> capitalizedIncomeClassificationOptions,
|
||||
final List<CodeValueData> buydownFeeClassificationOptions) {
|
||||
final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions, final List<CodeValueData> writeOffReasonOptions,
|
||||
final List<CodeValueData> capitalizedIncomeClassificationOptions, final List<CodeValueData> buydownFeeClassificationOptions) {
|
||||
|
||||
this.id = productData.id;
|
||||
this.name = productData.name;
|
||||
@@ -1134,6 +1155,8 @@ public class LoanProductData implements Serializable {
|
||||
this.feeToIncomeAccountMappings = productData.feeToIncomeAccountMappings;
|
||||
this.penaltyToIncomeAccountMappings = productData.penaltyToIncomeAccountMappings;
|
||||
this.chargeOffReasonToExpenseAccountMappings = productData.chargeOffReasonToExpenseAccountMappings;
|
||||
this.writeOffReasonsToExpenseMappings = productData.writeOffReasonsToExpenseMappings;
|
||||
this.writeOffReasonOptions = writeOffReasonOptions;
|
||||
|
||||
this.chargeOptions = chargeOptions;
|
||||
this.penaltyOptions = penaltyOptions;
|
||||
|
||||
+4
@@ -140,6 +140,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
||||
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveWriteOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element,
|
||||
loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
|
||||
@@ -237,6 +238,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
||||
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveWriteOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element,
|
||||
loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
|
||||
@@ -419,6 +421,8 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
||||
this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes);
|
||||
this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId,
|
||||
changes);
|
||||
|
||||
this.loanProductToGLAccountMappingHelper.updateWriteOffReasonToExpenseAccountMappings(command, element, loanProductId, changes);
|
||||
this.loanProductToGLAccountMappingHelper.updateBuyDownFeeClassificationToIncomeAccountMappings(command, element, loanProductId,
|
||||
changes);
|
||||
this.loanProductToGLAccountMappingHelper.updateCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
|
||||
|
||||
+9
-2
@@ -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.ClassificationToGLAccountData;
|
||||
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.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
@@ -355,6 +356,7 @@ public class LoanProductsApiResource {
|
||||
Collection<ChargeToGLAccountMapper> feeToGLAccountMappings;
|
||||
Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings;
|
||||
List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings;
|
||||
List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseAccountMappings;
|
||||
List<ClassificationToGLAccountData> capitalizedIncomeClassificationToGLAccountMappings;
|
||||
List<ClassificationToGLAccountData> buydowFeeClassificationToGLAccountMappings;
|
||||
if (loanProduct.hasAccountingEnabled()) {
|
||||
@@ -367,6 +369,8 @@ public class LoanProductsApiResource {
|
||||
.fetchPenaltyToIncomeAccountMappingsForLoanProduct(productId);
|
||||
chargeOffReasonToGLAccountMappings = this.accountMappingReadPlatformService
|
||||
.fetchChargeOffReasonMappingsForLoanProduct(productId);
|
||||
writeOffReasonsToExpenseAccountMappings = this.accountMappingReadPlatformService
|
||||
.fetchWriteOffReasonMappingsForLoanProduct(productId);
|
||||
capitalizedIncomeClassificationToGLAccountMappings = accountMappingReadPlatformService
|
||||
.fetchClassificationMappingsForLoanProduct(productId,
|
||||
LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
|
||||
@@ -374,7 +378,8 @@ public class LoanProductsApiResource {
|
||||
productId, LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
|
||||
loanProduct = LoanProductData.withAccountingDetails(loanProduct, accountingMappings, paymentChannelToFundSourceMappings,
|
||||
feeToGLAccountMappings, penaltyToGLAccountMappings, chargeOffReasonToGLAccountMappings,
|
||||
capitalizedIncomeClassificationToGLAccountMappings, buydowFeeClassificationToGLAccountMappings);
|
||||
writeOffReasonsToExpenseAccountMappings, capitalizedIncomeClassificationToGLAccountMappings,
|
||||
buydowFeeClassificationToGLAccountMappings);
|
||||
}
|
||||
|
||||
if (settings.isTemplate()) {
|
||||
@@ -475,6 +480,8 @@ public class LoanProductsApiResource {
|
||||
.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class);
|
||||
final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions = ApiFacingEnum
|
||||
.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class);
|
||||
final List<CodeValueData> writeOffReasonOptions = codeValueReadPlatformService
|
||||
.retrieveCodeValuesByCode(LoanApiConstants.WRITEOFFREASONS);
|
||||
final List<CodeValueData> capitalizedIncomeClassificationOptions = codeValueReadPlatformService
|
||||
.retrieveCodeValuesByCode(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE);
|
||||
final List<CodeValueData> buydownFeeClassificationOptions = codeValueReadPlatformService
|
||||
@@ -493,7 +500,7 @@ public class LoanProductsApiResource {
|
||||
creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions,
|
||||
daysInYearCustomStrategyOptions, capitalizedIncomeCalculationTypeOptions, capitalizedIncomeStrategyOptions,
|
||||
capitalizedIncomeTypeOptions, buyDownFeeCalculationTypeOptions, buyDownFeeStrategyOptions, buyDownFeeIncomeTypeOptions,
|
||||
capitalizedIncomeClassificationOptions, buydownFeeClassificationOptions);
|
||||
writeOffReasonOptions, capitalizedIncomeClassificationOptions, buydownFeeClassificationOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+50
-21
@@ -33,6 +33,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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_PENALTY.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.INCOME_FROM_CAPITALIZATION.getValue(),
|
||||
LoanProductAccountingParams.DEFERRED_INCOME_LIABILITY.getValue(), LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(),
|
||||
@@ -748,6 +751,7 @@ public final class LoanProductDataValidator {
|
||||
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
|
||||
validateChargeToIncomeAccountMappings(baseDataValidator, element);
|
||||
validateChargeOffToExpenseMappings(baseDataValidator, element);
|
||||
validateWriteOffToExpenseMappings(baseDataValidator, element);
|
||||
validateClassificationToIncomeMappings(baseDataValidator, element,
|
||||
LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
|
||||
validateClassificationToIncomeMappings(baseDataValidator, element,
|
||||
@@ -2068,54 +2072,79 @@ public final class LoanProductDataValidator {
|
||||
|
||||
private void validateChargeOffToExpenseMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
||||
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)) {
|
||||
final JsonArray chargeOffToExpenseMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element);
|
||||
if (chargeOffToExpenseMappingArray != null && chargeOffToExpenseMappingArray.size() > 0) {
|
||||
Map<Long, Set<Long>> chargeOffReasonToAccounts = new HashMap<>();
|
||||
final JsonArray reasonToExpenseMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element);
|
||||
if (reasonToExpenseMappingArray != null && !reasonToExpenseMappingArray.isEmpty()) {
|
||||
Map<Long, Set<Long>> reasonToAccounts = new HashMap<>();
|
||||
List<JsonObject> processedMappings = new ArrayList<>(); // Collect processed mappings for the new method
|
||||
|
||||
int i = 0;
|
||||
do {
|
||||
final JsonObject jsonObject = chargeOffToExpenseMappingArray.get(i).getAsJsonObject();
|
||||
final Long expenseGlAccountId = this.fromApiJsonHelper
|
||||
.extractLongNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject);
|
||||
final Long chargeOffReasonCodeValueId = this.fromApiJsonHelper
|
||||
.extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), jsonObject);
|
||||
final JsonObject jsonObject = reasonToExpenseMappingArray.get(i).getAsJsonObject();
|
||||
|
||||
final String expenseGlAccountIdString = this.fromApiJsonHelper
|
||||
.extractStringNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject);
|
||||
final String reasonCodeValueIdString = this.fromApiJsonHelper.extractStringNamed(reasonCodeValueIdParam.getValue(),
|
||||
jsonObject);
|
||||
|
||||
// Validate parameters locally
|
||||
baseDataValidator.reset()
|
||||
.parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT
|
||||
+ LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue())
|
||||
.value(expenseGlAccountId).notNull().integerGreaterThanZero();
|
||||
baseDataValidator.reset()
|
||||
.parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT
|
||||
+ LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue())
|
||||
.value(chargeOffReasonCodeValueId).notNull().integerGreaterThanZero();
|
||||
.value(expenseGlAccountIdString).notNull().longGreaterThanZero();
|
||||
baseDataValidator.reset().parameter(
|
||||
parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT + reasonCodeValueIdParam.getValue())
|
||||
.value(reasonCodeValueIdString).notNull().longGreaterThanZero();
|
||||
|
||||
// Handle duplicate charge-off reason and GL Account validation
|
||||
chargeOffReasonToAccounts.putIfAbsent(chargeOffReasonCodeValueId, new HashSet<>());
|
||||
Set<Long> associatedAccounts = chargeOffReasonToAccounts.get(chargeOffReasonCodeValueId);
|
||||
final Long reasonCodeValueId = Long.valueOf(reasonCodeValueIdString);
|
||||
final Long expenseGlAccountId = Long.valueOf(expenseGlAccountIdString);
|
||||
// Handle duplicate reason and GL Account validation
|
||||
reasonToAccounts.putIfAbsent(reasonCodeValueId, new HashSet<>());
|
||||
Set<Long> associatedAccounts = reasonToAccounts.get(reasonCodeValueId);
|
||||
|
||||
if (associatedAccounts.contains(expenseGlAccountId)) {
|
||||
baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET)
|
||||
.failWithCode("duplicate.chargeOffReason.and.glAccount");
|
||||
.failWithCode("duplicate." + failCode + ".and.glAccount");
|
||||
}
|
||||
associatedAccounts.add(expenseGlAccountId);
|
||||
|
||||
if (associatedAccounts.size() > 1) {
|
||||
baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET)
|
||||
.failWithCode("multiple.glAccounts.for.chargeOffReason");
|
||||
.failWithCode("multiple.glAccounts.for." + failCode);
|
||||
}
|
||||
|
||||
// Collect mapping for additional validations
|
||||
processedMappings.add(jsonObject);
|
||||
|
||||
i++;
|
||||
} while (i < chargeOffToExpenseMappingArray.size());
|
||||
} while (i < reasonToExpenseMappingArray.size());
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -606,7 +606,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
|
||||
loanChargeOffBehaviour.getValueAsStringEnumOptionData(), interestRecognitionOnDisbursementDate,
|
||||
daysInYearCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy,
|
||||
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/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/0199_write_off_reason_mapping_loan.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+34
@@ -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>
|
||||
+98
@@ -85,6 +85,7 @@ import org.apache.fineract.client.models.PostLoansResponse;
|
||||
import org.apache.fineract.client.models.PostRolesRequest;
|
||||
import org.apache.fineract.client.models.PostUsersRequest;
|
||||
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.PutLoansApprovedAmountResponse;
|
||||
import org.apache.fineract.client.models.PutLoansAvailableDisbursementAmountRequest;
|
||||
@@ -507,6 +508,103 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
.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() {
|
||||
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
|
||||
Assertions.assertNotNull(delinquencyBucketId);
|
||||
|
||||
+125
@@ -19,13 +19,23 @@
|
||||
package org.apache.fineract.integrationtests;
|
||||
|
||||
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.GetLoanProductsTemplateResponse;
|
||||
import org.apache.fineract.client.models.GetLoanProductsWriteOffReasonOptions;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.PostClientsResponse;
|
||||
import org.apache.fineract.client.models.PostCodeValueDataResponse;
|
||||
import org.apache.fineract.client.models.PostCodeValuesDataRequest;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||
import org.apache.fineract.client.models.PostWriteOffReasonToExpenseAccountMappings;
|
||||
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
|
||||
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.LoanBuyDownFeeIncomeType;
|
||||
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.Test;
|
||||
|
||||
@Slf4j
|
||||
public class LoanProductTest extends BaseLoanIntegrationTest {
|
||||
|
||||
@Nested
|
||||
@@ -458,4 +469,118 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -834,7 +834,7 @@ public class LoanProductTestBuilder {
|
||||
}
|
||||
Map<String, Long> newMap = new HashMap<>();
|
||||
newMap.put("chargeOffReasonCodeValueId", reasonId);
|
||||
newMap.put("expenseGLAccountId", accountId);
|
||||
newMap.put("expenseAccountId", accountId);
|
||||
this.chargeOffReasonToExpenseAccountMappings.add(newMap);
|
||||
return this;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user