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;
     }

Reply via email to