FINERACT-2358: Allow to configure advanced accounting rules based on write-off reason
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 12edabf..34442ba 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
@@ -69,6 +69,10 @@
     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 917fbdf..0ad7c3d 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 @@
             @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 @@
     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 dfbd94c..059ac48 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.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 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 void updateChargeOffReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
-            final Map<String, Object> changes, final PortfolioProductType portfolioProductType) {
+    private Long getReasonIdByCashAccountForLoan(final ProductToGLAccountMapping productToGLAccountMapping,
+            final CashAccountsForLoan cashAccountsForLoan) {
+        return switch (cashAccountsForLoan) {
+            case LOSSES_WRITTEN_OFF -> productToGLAccountMapping != null && productToGLAccountMapping.getWriteOffReason() != null
+                    ? productToGLAccountMapping.getWriteOffReason().getId()
+                    : null;
+            case CHARGE_OFF_EXPENSE -> productToGLAccountMapping != null && productToGLAccountMapping.getChargeOffReason() != null
+                    ? productToGLAccountMapping.getChargeOffReason().getId()
+                    : null;
+            default -> throw new IllegalStateException("Unexpected value: " + cashAccountsForLoan);
+        };
+    }
 
-        final List<ProductToGLAccountMapping> existingChargeOffReasonToGLAccountMappings = this.accountMappingRepository
-                .findAllChargeOffReasonsMappings(productId, portfolioProductType.getValue());
-        final JsonArray chargeOffReasonToGLAccountMappingArray = this.fromApiJsonHelper
-                .extractJsonArrayNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(), element);
+    public void updateReasonToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
+            final Map<String, Object> changes, final PortfolioProductType portfolioProductType,
+            final List<ProductToGLAccountMapping> existingReasonToGLAccountMappings,
+            final LoanProductAccountingParams reasonToExpenseAccountMappingsParam, final LoanProductAccountingParams reasonCodeValueIdParam,
+            final CashAccountsForLoan cashAccountsForLoan) {
 
-        final Map<Long, Long> inputChargeOffReasonToGLAccountMap = new HashMap<>();
+        final JsonArray reasonToGLAccountMappingArray = this.fromApiJsonHelper
+                .extractJsonArrayNamed(reasonToExpenseAccountMappingsParam.getValue(), element);
 
-        final Set<Long> existingChargeOffReasons = new HashSet<>();
-        if (chargeOffReasonToGLAccountMappingArray != null) {
+        final Map<Long, Long> inputReasonToGLAccountMap = new HashMap<>();
+
+        final Set<Long> existingReasons = new HashSet<>();
+        if (reasonToGLAccountMappingArray != null) {
             if (changes != null) {
-                changes.put(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(),
-                        command.jsonFragment(LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue()));
+                changes.put(reasonToExpenseAccountMappingsParam.getValue(),
+                        command.jsonFragment(reasonToExpenseAccountMappingsParam.getValue()));
             }
 
-            for (int i = 0; i < chargeOffReasonToGLAccountMappingArray.size(); i++) {
-                final JsonObject jsonObject = chargeOffReasonToGLAccountMappingArray.get(i).getAsJsonObject();
+            for (int i = 0; i < reasonToGLAccountMappingArray.size(); i++) {
+                final JsonObject jsonObject = reasonToGLAccountMappingArray.get(i).getAsJsonObject();
                 final Long expenseGlAccountId = jsonObject.get(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue()).getAsLong();
-                final Long chargeOffReasonCodeValueId = jsonObject
-                        .get(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue()).getAsLong();
-                inputChargeOffReasonToGLAccountMap.put(chargeOffReasonCodeValueId, expenseGlAccountId);
+                final Long reasonCodeValueId = jsonObject.get(reasonCodeValueIdParam.getValue()).getAsLong();
+                inputReasonToGLAccountMap.put(reasonCodeValueId, expenseGlAccountId);
             }
 
             // If input map is empty, delete all existing mappings
-            if (inputChargeOffReasonToGLAccountMap.isEmpty()) {
-                this.accountMappingRepository.deleteAll(existingChargeOffReasonToGLAccountMappings);
+            if (inputReasonToGLAccountMap.isEmpty()) {
+                this.accountMappingRepository.deleteAll(existingReasonToGLAccountMappings);
+
             } else {
-                for (final ProductToGLAccountMapping existingChargeOffReasonToGLAccountMapping : existingChargeOffReasonToGLAccountMappings) {
-                    final Long currentChargeOffReasonId = existingChargeOffReasonToGLAccountMapping.getChargeOffReason().getId();
-                    if (currentChargeOffReasonId != null) {
-                        existingChargeOffReasons.add(currentChargeOffReasonId);
+                for (final ProductToGLAccountMapping existingReasonToGLAccountMapping : existingReasonToGLAccountMappings) {
+                    final Long currentReasonId = getReasonIdByCashAccountForLoan(existingReasonToGLAccountMapping, cashAccountsForLoan);
+                    if (currentReasonId != null) {
+                        existingReasons.add(currentReasonId);
                         // update existing mappings (if required)
-                        if (inputChargeOffReasonToGLAccountMap.containsKey(currentChargeOffReasonId)) {
-                            final Long newGLAccountId = inputChargeOffReasonToGLAccountMap.get(currentChargeOffReasonId);
-                            if (!newGLAccountId.equals(existingChargeOffReasonToGLAccountMapping.getGlAccount().getId())) {
+                        if (inputReasonToGLAccountMap.containsKey(currentReasonId)) {
+                            final Long newGLAccountId = inputReasonToGLAccountMap.get(currentReasonId);
+                            if (!newGLAccountId.equals(existingReasonToGLAccountMapping.getGlAccount().getId())) {
                                 final Optional<GLAccount> glAccount = accountRepository.findById(newGLAccountId);
                                 if (glAccount.isPresent()) {
-                                    existingChargeOffReasonToGLAccountMapping.setGlAccount(glAccount.get());
-                                    this.accountMappingRepository.saveAndFlush(existingChargeOffReasonToGLAccountMapping);
+                                    existingReasonToGLAccountMapping.setGlAccount(glAccount.get());
+                                    this.accountMappingRepository.saveAndFlush(existingReasonToGLAccountMapping);
                                 }
                             }
                         } // deleted payment type
                         else {
-                            this.accountMappingRepository.delete(existingChargeOffReasonToGLAccountMapping);
+                            this.accountMappingRepository.delete(existingReasonToGLAccountMapping);
                         }
                     }
                 }
 
                 // only the newly added
-                for (Map.Entry<Long, Long> entry : inputChargeOffReasonToGLAccountMap.entrySet().stream()
-                        .filter(e -> !existingChargeOffReasons.contains(e.getKey())).toList()) {
-                    saveChargeOffReasonToExpenseMapping(productId, entry.getKey(), entry.getValue(), portfolioProductType);
+                for (Map.Entry<Long, Long> entry : inputReasonToGLAccountMap.entrySet().stream()
+                        .filter(e -> !existingReasons.contains(e.getKey())).toList()) {
+                    saveReasonToExpenseMapping(productId, entry.getKey(), entry.getValue(), portfolioProductType, cashAccountsForLoan);
                 }
             }
         }
@@ -587,21 +604,37 @@
         this.accountMappingRepository.saveAndFlush(accountMapping);
     }
 
-    private void saveChargeOffReasonToExpenseMapping(final Long productId, final Long reasonId, final Long expenseAccountId,
-            final PortfolioProductType portfolioProductType) {
+    private Predicate<? super ProductToGLAccountMapping> matching(final CashAccountsForLoan typeDef, final Long reasonId) {
+        return switch (typeDef) {
+            case CHARGE_OFF_EXPENSE -> (mapping) -> (mapping.getChargeOffReason() != null && mapping.getChargeOffReason().getId() != null
+                    && mapping.getChargeOffReason().getId().equals(reasonId));
+            case LOSSES_WRITTEN_OFF -> (mapping) -> (mapping.getWriteOffReason() != null && mapping.getWriteOffReason().getId() != null
+                    && mapping.getWriteOffReason().getId().equals(reasonId));
+            default -> throw new IllegalStateException("Unexpected value: " + typeDef);
+        };
+    }
+
+    private void saveReasonToExpenseMapping(final Long productId, final Long reasonId, final Long expenseAccountId,
+            final PortfolioProductType portfolioProductType, final CashAccountsForLoan cashAccountsForLoan) {
 
         final Optional<GLAccount> glAccount = accountRepository.findById(expenseAccountId);
-
-        final boolean reasonMappingExists = this.accountMappingRepository
-                .findAllChargeOffReasonsMappings(productId, portfolioProductType.getValue()).stream()
-                .anyMatch(mapping -> mapping.getChargeOffReason().getId().equals(reasonId));
-
         final Optional<CodeValue> codeValueOptional = codeValueRepository.findById(reasonId);
 
-        if (glAccount.isPresent() && !reasonMappingExists && codeValueOptional.isPresent()) {
+        final boolean reasonMappingExists = this.accountMappingRepository
+                .findAllProductToGLAccountMappingsByProductIdAndProductTypeAndFinancialAccountType(productId,
+                        portfolioProductType.getValue(), cashAccountsForLoan.getValue())
+                .stream().anyMatch(matching(cashAccountsForLoan, reasonId));
+
+        if (!reasonMappingExists && glAccount.isPresent() && codeValueOptional.isPresent()) {
             final ProductToGLAccountMapping accountMapping = new ProductToGLAccountMapping().setGlAccount(glAccount.get())
                     .setProductId(productId).setProductType(portfolioProductType.getValue())
-                    .setFinancialAccountType(CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue()).setChargeOffReason(codeValueOptional.get());
+                    .setFinancialAccountType(cashAccountsForLoan.getValue());
+
+            switch (cashAccountsForLoan) {
+                case CHARGE_OFF_EXPENSE -> accountMapping.setChargeOffReason(codeValueOptional.get());
+                case LOSSES_WRITTEN_OFF -> accountMapping.setWriteOffReason(codeValueOptional.get());
+                default -> throw new IllegalStateException("Unexpected value: " + cashAccountsForLoan);
+            }
 
             this.accountMappingRepository.saveAndFlush(accountMapping);
         }
@@ -703,22 +736,25 @@
         }
     }
 
-    public void validateChargeOffMappingsInDatabase(final List<JsonObject> mappings) {
-        final List<ApiParameterError> validationErrors = new ArrayList<>();
+    public void validateWriteOffMappingsInDatabase(final List<ApiParameterError> validationErrors, final List<JsonObject> mappings) {
+        for (JsonObject jsonObject : mappings) {
+            final Long writeOffReasonCodeValueId = this.fromApiJsonHelper
+                    .extractLongNamed(LoanProductAccountingParams.WRITE_OFF_REASON_CODE_VALUE_ID.getValue(), jsonObject);
 
+            // Validation: writeOffReasonCodeValueId must exist in the database
+            CodeValue codeValue = this.codeValueRepository.findByCodeNameAndId("WriteOffReasons", writeOffReasonCodeValueId);
+            if (codeValue == null) {
+                validationErrors.add(ApiParameterError.parameterError("validation.msg.writeoffreason.invalid",
+                        "Write-off reason with ID " + writeOffReasonCodeValueId + " does not exist",
+                        LoanProductAccountingParams.WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue()));
+            }
+        }
+    }
+
+    public void validateGLAccountInDatabase(final List<ApiParameterError> validationErrors, final List<JsonObject> mappings) {
         for (JsonObject jsonObject : mappings) {
             final Long expenseGlAccountId = this.fromApiJsonHelper
                     .extractLongNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject);
-            final Long chargeOffReasonCodeValueId = this.fromApiJsonHelper
-                    .extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), jsonObject);
-
-            // Validation: chargeOffReasonCodeValueId must exist in the database
-            CodeValue codeValue = this.codeValueRepository.findByCodeNameAndId("ChargeOffReasons", chargeOffReasonCodeValueId);
-            if (codeValue == null) {
-                validationErrors.add(ApiParameterError.parameterError("validation.msg.chargeoffreason.invalid",
-                        "Charge-off reason with ID " + chargeOffReasonCodeValueId + " does not exist",
-                        LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue()));
-            }
 
             // Validation: expenseGLAccountId must exist as a valid Expense GL account
             final Optional<GLAccount> glAccount = accountRepository.findById(expenseGlAccountId);
@@ -730,6 +766,22 @@
 
             }
         }
+    }
+
+    public 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 22ccc37..d7af51f 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.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 @@
 
     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 1fcb7bf..078b2b8 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.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 @@
         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
@@ -368,6 +385,11 @@
     }
 
     @Override
+    public List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappingsForLoanProduct(Long loanProductId) {
+        return fetchWriteOffReasonMappings(PortfolioProductType.LOAN, loanProductId);
+    }
+
+    @Override
     public List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId,
             LoanProductAccountingParams classificationParameter) {
         return fetchClassificationMappings(PortfolioProductType.LOAN, loanProductId, 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 7435a46..b05b2d6 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 @@
         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 0000000..df49b8e
--- /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 13ab2a2..253b4a8 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 @@
         }
 
         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 80b6427..ecffdb7 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.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 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 ccb47b2..79c007b 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 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 @@
         @Schema(example = "REGULAR")
         public String chargeOffBehaviour;
 
-        static final class PostChargeOffReasonToExpenseAccountMappings {
+        public static final class PostChargeOffReasonToExpenseAccountMappings {
 
             private PostChargeOffReasonToExpenseAccountMappings() {}
 
@@ -382,6 +383,17 @@
             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 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 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 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 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 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 @@
         @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 87660df..23066f3 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.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 @@
     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 @@
         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 @@
                 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 @@
         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 @@
                 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 @@
         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 @@
                 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 @@
         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 @@
                 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 @@
             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 @@
         productData.feeToIncomeAccountMappings = feeToGLAccountMappings;
         productData.penaltyToIncomeAccountMappings = penaltyToGLAccountMappings;
         productData.chargeOffReasonToExpenseAccountMappings = chargeOffReasonToGLAccountMappings;
+        productData.writeOffReasonsToExpenseMappings = writeOffReasonToGLAccountMappings;
         productData.capitalizedIncomeClassificationToIncomeAccountMappings = capitalizedIncomeClassificationToIncomeAccountMappings;
         productData.buydownFeeClassificationToIncomeAccountMappings = buydownFeeClassificationToIncomeAccountMappings;
         return productData;
@@ -884,7 +900,9 @@
             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 @@
         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 @@
         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 @@
             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 @@
         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 8ed7066..5e29e5c 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 @@
                 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 @@
                 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 @@
             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 6c981ba..70afeda 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.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 @@
         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 @@
                     .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 @@
                     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 @@
                 .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 @@
                 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 cd99af2..9132f39 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.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 @@
             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 @@
             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 @@
 
     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 37e9987..4444f42 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 @@
                     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 7b12aaa..b2c97bf 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 0000000..74cdd5a
--- /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 47c6414..df8cc9d 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.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 @@
                 .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 28c144f..060da1b 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.Nested;
 import org.junit.jupiter.api.Test;
 
+@Slf4j
 public class LoanProductTest extends BaseLoanIntegrationTest {
 
     @Nested
@@ -458,4 +469,118 @@
                             .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 43b34bf..0ae33fa 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 @@
         }
         Map<String, Long> newMap = new HashMap<>();
         newMap.put("chargeOffReasonCodeValueId", reasonId);
-        newMap.put("expenseGLAccountId", accountId);
+        newMap.put("expenseAccountId", accountId);
         this.chargeOffReasonToExpenseAccountMappings.add(newMap);
         return this;
     }