This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new d6323be7dd FINERACT-2358: Allow to configure advanced accounting rules
based on write-off reason
d6323be7dd is described below
commit d6323be7dd690a18e33107dcd239ea5f8e6e8126
Author: Soma Sörös <[email protected]>
AuthorDate: Thu Sep 18 15:08:55 2025 +0200
FINERACT-2358: Allow to configure advanced accounting rules based on
write-off reason
---
.../domain/ProductToGLAccountMapping.java | 4 +
.../ProductToGLAccountMappingRepository.java | 11 +-
.../service/ProductToGLAccountMappingHelper.java | 172 ++++++++++++++-------
...oductToGLAccountMappingReadPlatformService.java | 3 +
...tToGLAccountMappingReadPlatformServiceImpl.java | 22 +++
.../accounting/common/AccountingConstants.java | 2 +
.../WriteOffReasonsToExpenseAccountMapper.java | 36 +++++
.../core/data/DataValidatorBuilder.java | 21 ++-
.../LoanProductToGLAccountMappingHelper.java | 32 +++-
.../api/LoanProductsApiResourceSwagger.java | 49 +++++-
.../loanproduct/data/LoanProductData.java | 39 ++++-
...ToGLAccountMappingWritePlatformServiceImpl.java | 4 +
.../loanproduct/api/LoanProductsApiResource.java | 11 +-
.../serialization/LoanProductDataValidator.java | 71 ++++++---
.../LoanProductReadPlatformServiceImpl.java | 2 +-
.../db/changelog/tenant/changelog-tenant.xml | 1 +
.../parts/0199_write_off_reason_mapping_loan.xml | 34 ++++
.../integrationtests/BaseLoanIntegrationTest.java | 98 ++++++++++++
.../fineract/integrationtests/LoanProductTest.java | 125 +++++++++++++++
.../common/loans/LoanProductTestBuilder.java | 2 +-
20 files changed, 634 insertions(+), 105 deletions(-)
diff --git
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java
index 12edabfae4..34442ba216 100644
---
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java
+++
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java
@@ -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;
diff --git
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java
index 917fbdffd7..0ad7c3d152 100644
---
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java
+++
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java
@@ -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")
diff --git
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java
index dfbd94c529..059ac48451 100644
---
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java
+++
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java
@@ -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);
+ };
+ }
+
+ 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 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);
+ final JsonArray reasonToGLAccountMappingArray = this.fromApiJsonHelper
+
.extractJsonArrayNamed(reasonToExpenseAccountMappingsParam.getValue(), element);
- final Map<Long, Long> inputChargeOffReasonToGLAccountMap = new
HashMap<>();
+ final Map<Long, Long> inputReasonToGLAccountMap = new HashMap<>();
- final Set<Long> existingChargeOffReasons = new HashSet<>();
- if (chargeOffReasonToGLAccountMappingArray != null) {
+ 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 Optional<CodeValue> codeValueOptional =
codeValueRepository.findById(reasonId);
final boolean reasonMappingExists = this.accountMappingRepository
- .findAllChargeOffReasonsMappings(productId,
portfolioProductType.getValue()).stream()
- .anyMatch(mapping ->
mapping.getChargeOffReason().getId().equals(reasonId));
-
- final Optional<CodeValue> codeValueOptional =
codeValueRepository.findById(reasonId);
+
.findAllProductToGLAccountMappingsByProductIdAndProductTypeAndFinancialAccountType(productId,
+ portfolioProductType.getValue(),
cashAccountsForLoan.getValue())
+ .stream().anyMatch(matching(cashAccountsForLoan, reasonId));
- if (glAccount.isPresent() && !reasonMappingExists &&
codeValueOptional.isPresent()) {
+ 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 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 Long writeOffReasonCodeValueId = this.fromApiJsonHelper
+
.extractLongNamed(LoanProductAccountingParams.WRITE_OFF_REASON_CODE_VALUE_ID.getValue(),
jsonObject);
- // Validation: chargeOffReasonCodeValueId must exist in the
database
- CodeValue codeValue =
this.codeValueRepository.findByCodeNameAndId("ChargeOffReasons",
chargeOffReasonCodeValueId);
+ // Validation: writeOffReasonCodeValueId must exist in the database
+ CodeValue codeValue =
this.codeValueRepository.findByCodeNameAndId("WriteOffReasons",
writeOffReasonCodeValueId);
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()));
+
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);
// 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()) {
diff --git
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java
index 22ccc37c13..d7af51f08c 100644
---
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java
+++
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java
@@ -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);
}
diff --git
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java
index 1fcb7bf6d1..078b2b8104 100644
---
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java
+++
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java
@@ -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) {
diff --git
a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java
b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java
index 7435a46d90..b05b2d6ae8 100644
---
a/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java
+++
b/fineract-core/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java
@@ -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"), //
diff --git
a/fineract-core/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/data/WriteOffReasonsToExpenseAccountMapper.java
b/fineract-core/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/data/WriteOffReasonsToExpenseAccountMapper.java
new file mode 100644
index 0000000000..df49b8e1b7
--- /dev/null
+++
b/fineract-core/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/data/WriteOffReasonsToExpenseAccountMapper.java
@@ -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;
+}
diff --git
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
index 13ab2a2ea5..253b4a8575 100644
---
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
@@ -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;
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java
b/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java
index 80b64277e4..ecffdb7dba 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/LoanProductToGLAccountMappingHelper.java
@@ -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,
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java
index ccb47b2ffc..79c007bb59 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java
@@ -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;
+ }
+
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java
index 87660df6d0..23066f3afc 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java
@@ -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;
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java
index 8ed70663e7..5e29e5ca16 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/accounting/productaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java
@@ -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,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java
index 6c981ba69a..70afeda88e 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java
@@ -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);
}
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java
index cd99af21d7..9132f39f63 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java
@@ -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);
+ }
}
}
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java
index 37e9987e2a..4444f42ade 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java
@@ -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);
}
}
diff --git
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index 7b12aaab7d..b2c97bf236 100644
---
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -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>
diff --git
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0199_write_off_reason_mapping_loan.xml
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0199_write_off_reason_mapping_loan.xml
new file mode 100644
index 0000000000..74cdd5aea4
--- /dev/null
+++
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0199_write_off_reason_mapping_loan.xml
@@ -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>
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index 47c64147bb..df8cc9d843 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -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);
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductTest.java
index 28c144f26b..060da1bc63 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductTest.java
@@ -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();
+ }
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
index 43b34bf859..0ae33fa693 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
@@ -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;
}