You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2023/01/06 12:42:58 UTC

[fineract] branch develop updated: FINERACT-1806: Charge-off API

This is an automated email from the ASF dual-hosted git repository.

arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new c7504680b FINERACT-1806: Charge-off API
c7504680b is described below

commit c7504680b76c55d685d12d1a6edf995544881c7c
Author: Adam Saghy <ad...@gmail.com>
AuthorDate: Thu Jan 5 09:36:40 2023 +0100

    FINERACT-1806: Charge-off API
---
 .../src/main/avro/loan/v1/LoanAccountDataV1.avsc   | 24 ++++++++++
 .../loan/v1/LoanApplicationTimelineDataV1.avsc     | 32 +++++++++++++
 .../src/main/avro/loan/v1/LoanSummaryDataV1.avsc   | 16 +++++++
 .../commands/service/CommandWrapperBuilder.java    |  8 ++++
 ...etailsReadPlatformServiceJpaRepositoryImpl.java | 17 +++++--
 .../loanaccount/api/LoanApiConstants.java          |  4 +-
 .../api/LoanTransactionsApiResource.java           | 15 ++++--
 .../api/LoanTransactionsApiResourceSwagger.java    |  5 +-
 .../loanaccount/api/LoansApiResourceSwagger.java   | 14 ++++++
 .../loanaccount/data/LoanAccountData.java          | 26 +++++-----
 .../data/LoanApplicationTimelineData.java          | 12 ++++-
 .../loanaccount/data/LoanSummaryData.java          | 19 +++++---
 .../loanaccount/data/LoanTransactionData.java      |  6 +++
 .../portfolio/loanaccount/domain/Loan.java         | 33 +++++++++++++
 .../loanaccount/domain/LoanTransaction.java        | 26 +++++++++-
 .../loanaccount/domain/LoanTransactionType.java    |  7 ++-
 .../handler/ChargeOffLoanCommandHandler.java       | 43 +++++++++++++++++
 .../serialization/LoanEventApiJsonValidator.java   | 30 ++++++++++++
 .../service/LoanReadPlatformService.java           |  2 +
 .../service/LoanReadPlatformServiceImpl.java       | 42 +++++++++++++++--
 .../service/LoanWritePlatformService.java          |  1 +
 .../LoanWritePlatformServiceJpaRepositoryImpl.java | 52 ++++++++++++++++++++
 .../loanproduct/service/LoanEnumerations.java      |  2 +
 .../api/SelfLoansApiResourceSwagger.java           | 16 +++----
 .../db/changelog/tenant/changelog-tenant.xml       |  1 +
 .../parts/0079_add_charge_off_details_to_loan.xml  | 50 ++++++++++++++++++++
 .../ClientLoanIntegrationTest.java                 | 55 ++++++++++++++++++++++
 .../ExternalIdSupportIntegrationTest.java          | 18 +++++++
 .../common/loans/LoanTransactionHelper.java        |  8 ++++
 .../integrationtests/common/system/CodeHelper.java |  8 ++++
 30 files changed, 549 insertions(+), 43 deletions(-)

diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc
index ad628df93..1ce3178fa 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc
@@ -724,6 +724,30 @@
                 "null",
                 "org.apache.fineract.avro.loan.v1.DelinquencyRangeDataV1"
             ]
+        },
+        {
+            "default": null,
+            "name": "overpaidOnDate",
+            "type": [
+                "null",
+                "string"
+            ]
+        },
+        {
+            "default": null,
+            "name": "lastClosedBusinessDate",
+            "type": [
+                "null",
+                "string"
+            ]
+        },
+        {
+            "default": null,
+            "name": "chargedOff",
+            "type": [
+                "null",
+                "boolean"
+            ]
         }
     ]
 }
diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanApplicationTimelineDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanApplicationTimelineDataV1.avsc
index 880a2f7f8..4066d5bd5 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanApplicationTimelineDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanApplicationTimelineDataV1.avsc
@@ -242,6 +242,38 @@
                 "null",
                 "string"
             ]
+        },
+        {
+            "default": null,
+            "name": "chargedOffOnDate",
+            "type": [
+                "null",
+                "string"
+            ]
+        },
+        {
+            "default": null,
+            "name": "chargedOffByUsername",
+            "type": [
+                "null",
+                "string"
+            ]
+        },
+        {
+            "default": null,
+            "name": "chargedOffByFirstname",
+            "type": [
+                "null",
+                "string"
+            ]
+        },
+        {
+            "default": null,
+            "name": "chargedOffByLastname",
+            "type": [
+                "null",
+                "string"
+            ]
         }
     ]
 }
diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc
index 951d4cf78..6e62713f4 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc
@@ -410,6 +410,22 @@
                 "null",
                 "bigdecimal"
             ]
+        },
+        {
+            "default": null,
+            "name": "chargeOffReasonId",
+            "type": [
+                "null",
+                "long"
+            ]
+        },
+        {
+            "default": null,
+            "name": "chargeOffReason",
+            "type": [
+                "null",
+                "string"
+            ]
         }
     ]
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index 4ccc6a0ff..4a2837084 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -3606,4 +3606,12 @@ public class CommandWrapperBuilder {
         this.href = "/externaleventconfiguration";
         return this;
     }
+
+    public CommandWrapperBuilder chargeOff(final Long loanId) {
+        this.actionName = "CHARGEOFF";
+        this.entityName = "LOAN";
+        this.loanId = loanId;
+        this.href = "/loans/" + loanId + "/transactions?command=charge-off";
+        return this;
+    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java
index 0a8ffcae1..0dded0e2b 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/accountdetails/service/AccountDetailsReadPlatformServiceJpaRepositoryImpl.java
@@ -470,15 +470,17 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
                     .append(" l.closedon_date as closedOnDate,")
                     .append(" cbu.username as closedByUsername, cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname,")
                     .append(" la.overdue_since_date_derived as overdueSinceDate, ")
-                    .append(" l.writtenoffon_date as writtenOffOnDate, l.expected_maturedon_date as expectedMaturityDate")
-
-                    .append(" from m_loan l ").append("LEFT JOIN m_product_loan AS lp ON lp.id = l.product_id")
+                    .append(" l.writtenoffon_date as writtenOffOnDate, l.expected_maturedon_date as expectedMaturityDate, ")
+                    .append(" l.charged_off_on_date as chargedOffOnDate, cobu.username as chargedOffByUsername, ")
+                    .append(" cobu.firstname as chargedOffByFirstname, cobu.lastname as chargedOffByLastname ").append(" from m_loan l ")
+                    .append("LEFT JOIN m_product_loan AS lp ON lp.id = l.product_id")
                     .append(" left join m_appuser sbu on sbu.id = l.created_by")
                     .append(" left join m_appuser rbu on rbu.id = l.rejectedon_userid")
                     .append(" left join m_appuser wbu on wbu.id = l.withdrawnon_userid")
                     .append(" left join m_appuser abu on abu.id = l.approvedon_userid")
                     .append(" left join m_appuser dbu on dbu.id = l.disbursedon_userid")
                     .append(" left join m_appuser cbu on cbu.id = l.closedon_userid")
+                    .append(" left join m_appuser cobu on cobu.id = l.charged_off_by_userid")
                     .append(" left join m_loan_arrears_aging la on la.loan_id = l.id")
                     .append(" left join glim_accounts glim on glim.id=l.glim_id");
 
@@ -541,6 +543,12 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
             final LocalDate expectedMaturityDate = JdbcSupport.getLocalDate(rs, "expectedMaturityDate");
 
             final LocalDate overdueSinceDate = JdbcSupport.getLocalDate(rs, "overdueSinceDate");
+
+            final LocalDate chargedOffOnDate = JdbcSupport.getLocalDate(rs, "chargedOffOnDate");
+            final String chargedOffByUsername = rs.getString("chargedOffByUsername");
+            final String chargedOffByFirstname = rs.getString("chargedOffByFirstname");
+            final String chargedOffByLastname = rs.getString("chargedOffByLastname");
+
             Boolean inArrears = (overdueSinceDate != null);
 
             final LoanApplicationTimelineData timeline = new LoanApplicationTimelineData(submittedOnDate, submittedByUsername,
@@ -548,7 +556,8 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
                     withdrawnOnDate, withdrawnByUsername, withdrawnByFirstname, withdrawnByLastname, approvedOnDate, approvedByUsername,
                     approvedByFirstname, approvedByLastname, expectedDisbursementDate, actualDisbursementDate, disbursedByUsername,
                     disbursedByFirstname, disbursedByLastname, closedOnDate, closedByUsername, closedByFirstname, closedByLastname,
-                    expectedMaturityDate, writtenOffOnDate, closedByUsername, closedByFirstname, closedByLastname);
+                    expectedMaturityDate, writtenOffOnDate, closedByUsername, closedByFirstname, closedByLastname, chargedOffOnDate,
+                    chargedOffByUsername, chargedOffByFirstname, chargedOffByLastname);
 
             return new LoanAccountSummaryData(id, accountNo, parentAccountNumber, externalId, productId, loanProductName,
                     shortLoanProductName, loanStatus, loanType, loanCycle, timeline, inArrears, originalLoan, loanBalance, amountPaid);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java
index 82962c8c1..a439a5fbf 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java
@@ -133,7 +133,8 @@ public interface LoanApiConstants {
     String installmentAmountParamName = "installmentAmount";
     // loan write off
     String WRITEOFFREASONS = "WriteOffReasons";
-
+    // loan charge-off
+    String CHARGE_OFF_REASONS = "ChargeOffReasons";
     // fore closure constants
     String transactionDateParamName = "transactionDate";
     String noteParamName = "note";
@@ -153,6 +154,7 @@ public interface LoanApiConstants {
     String fixedPrincipalPercentagePerInstallmentParamName = "fixedPrincipalPercentagePerInstallment";
 
     String LOAN_ASSOCIATIONS_ALL = "all";
+    String chargeOffReasonIdParamName = "chargeOffReasonId";
 
     // Reversal Transation Data
     String REVERSAL_EXTERNAL_ID_PARAMNAME = "reversalExternalId";
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
index c5afc8c7a..f7bdb4c8a 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
@@ -76,6 +76,7 @@ import org.springframework.stereotype.Component;
 @Tag(name = "Loan Transactions", description = "Capabilities include loan repayment's, interest waivers and the ability to 'adjust' an existing transaction. An 'adjustment' of a transaction is really a 'reversal' of existing transaction followed by creation of a new transaction with the provided details.")
 public class LoanTransactionsApiResource {
 
+    public static final String CHARGE_OFF_COMMAND_VALUE = "charge-off";
     private final Set<String> responseDataParameters = new HashSet<>(Arrays.asList("id", "type", "date", "currency", "amount", "externalId",
             LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, LoanApiConstants.REVERSED_ON_DATE_PARAMNAME));
 
@@ -103,7 +104,8 @@ public class LoanTransactionsApiResource {
             + "loans/1/transactions/template?command=recoverypayment" + "\n" + "loans/1/transactions/template?command=prepayLoan" + "\n"
             + "loans/1/transactions/template?command=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n"
             + "loans/1/transactions/template?command=foreclosure" + "\n"
-            + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)")
+            + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)" + "\n"
+            + "loans/1/transactions/template?command=charge-off" + "\n")
     @ApiResponses({
             @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
     public String retrieveTransactionTemplate(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
@@ -129,7 +131,8 @@ public class LoanTransactionsApiResource {
             + "loans/1/transactions/template?command=recoverypayment" + "\n" + "loans/1/transactions/template?command=prepayLoan" + "\n"
             + "loans/1/transactions/template?command=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n"
             + "loans/1/transactions/template?command=foreclosure" + "\n"
-            + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)")
+            + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)" + "\n"
+            + "loans/1/transactions/template?command=charge-off" + "\n")
     @ApiResponses({
             @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
     public String retrieveTransactionTemplate(
@@ -224,7 +227,8 @@ public class LoanTransactionsApiResource {
             + " | Undo Loan Write-off | \n" + "loans/1/transactions?command=recoverypayment" + " | Make Recovery Payment | \n"
             + "loans/1/transactions?command=refundByCash" + " | Make a Refund of an Active Loan by Cash | \n"
             + "loans/1/transactions?command=foreclosure" + " | Foreclosure of an Active Loan | \n"
-            + "loans/1/transactions?command=creditBalanceRefund" + " | Credit Balance Refund" + " |  \n")
+            + "loans/1/transactions?command=creditBalanceRefund" + " | Credit Balance Refund" + " |  \n"
+            + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=charge-off" + " | Charge-off Loan" + " |  \n")
     @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsRequest.class)))
     @ApiResponses({
             @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsResponse.class))) })
@@ -257,6 +261,7 @@ public class LoanTransactionsApiResource {
             + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=foreclosure"
             + " | Foreclosure of an Active Loan | \n"
             + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=creditBalanceRefund" + " | Credit Balance Refund"
+            + " |  \n" + "loans/external-id/7dd80a7c-ycba-a446-t378-91eb6f53e854/transactions?command=charge-off" + " | Charge-off Loan"
             + " |  \n")
     @RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsRequest.class)))
     @ApiResponses({
@@ -456,6 +461,8 @@ public class LoanTransactionsApiResource {
             commandRequest = builder.loanForeclosure(resolvedLoanId).build();
         } else if (CommandParameterUtil.is(commandParam, "creditBalanceRefund")) {
             commandRequest = builder.creditBalanceRefund(resolvedLoanId).build();
+        } else if (CommandParameterUtil.is(commandParam, CHARGE_OFF_COMMAND_VALUE)) {
+            commandRequest = builder.chargeOff(resolvedLoanId).build();
         }
 
         if (commandRequest == null) {
@@ -529,6 +536,8 @@ public class LoanTransactionsApiResource {
             transactionData = this.loanReadPlatformService.retrieveLoanForeclosureTemplate(resolvedLoanId, transactionDate);
         } else if (CommandParameterUtil.is(commandParam, "creditBalanceRefund")) {
             transactionData = this.loanReadPlatformService.retrieveCreditBalanceRefundTemplate(resolvedLoanId);
+        } else if (CommandParameterUtil.is(commandParam, CHARGE_OFF_COMMAND_VALUE)) {
+            transactionData = this.loanReadPlatformService.retrieveLoanChargeOffTemplate(resolvedLoanId);
         } else {
             throw new UnrecognizedQueryParamException("command", commandParam);
         }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
index d4fb22220..72a2ab8d0 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
@@ -258,7 +258,10 @@ final class LoanTransactionsApiResourceSwagger {
         public Integer loanChargeId;
         @Schema(example = "28 June 2022")
         public String dueDate;
-
+        @Schema(example = "1")
+        public Long chargeOffReasonId;
+        @Schema(example = "1")
+        public Long writeoffReasonId;
     }
 
     @Schema(description = "PostLoansLoanIdTransactionsResponse")
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
index caaae16de..ebf74eeb6 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
@@ -247,6 +247,14 @@ final class LoansApiResourceSwagger {
             public LocalDate expectedMaturityDate;
             @Schema(example = "[2012, 4, 3]")
             public LocalDate closedOnDate;
+            @Schema(example = "[2012, 4, 10]")
+            public LocalDate chargedOffOnDate;
+            @Schema(example = "admin")
+            public String chargedOffByUsername;
+            @Schema(example = "App")
+            public String chargedOffByFirstname;
+            @Schema(example = "Administrator")
+            public String chargedOffByLastname;
         }
 
         static final class GetLoansLoanIdRepaymentSchedule {
@@ -601,6 +609,10 @@ final class LoansApiResourceSwagger {
             @Schema(example = "0.000000")
             public Double totalRepaymentTransactionReversed;
             public Set<GetLoansLoanIdOverdueCharges> overdueCharges;
+            @Schema(example = "1")
+            public Long chargeOffReasonId;
+            @Schema(example = "reason")
+            public String chargeOffReason;
         }
 
         static final class GetLoansLoanIdPaymentType {
@@ -1028,6 +1040,8 @@ final class LoansApiResourceSwagger {
         public LocalDate lastClosedBusinessDate;
         @Schema(example = "[2013, 11, 1]")
         public LocalDate overpaidOnDate;
+        @Schema(example = "false")
+        public Boolean chargedOff;
     }
 
     @Schema(description = "GetLoansResponse")
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java
index 1953568c4..86b67aa10 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java
@@ -248,11 +248,12 @@ public class LoanAccountData {
     private String linkAccountId;
     private Long groupId;
     private LocalDate expectedDisbursementDate;
-    private LocalDate overpaidOnDate;
 
+    private LocalDate overpaidOnDate;
     private CollectionData delinquent;
     private DelinquencyRangeData delinquencyRange;
     private LocalDate lastClosedBusinessDate;
+    private Boolean chargedOff;
 
     public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEnumOption, Long clientId, Long productId,
             Long loanOfficerId, LocalDate submittedOnDate, Long fundId, BigDecimal principal, Integer numberOfRepayments,
@@ -381,7 +382,7 @@ public class LoanAccountData {
                 .setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
                 .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
                 .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
-                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate);
+                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff);
     }
 
     /**
@@ -452,7 +453,7 @@ public class LoanAccountData {
                 .setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
                 .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
                 .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
-                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate);
+                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff);
     }
 
     public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product,
@@ -620,7 +621,7 @@ public class LoanAccountData {
                 .setIsEqualAmortization(product.isEqualAmortization()).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
                 .setFixedPrincipalPercentagePerInstallment(product.getFixedPrincipalPercentagePerInstallment()).setDelinquent(delinquent)
                 .setDisallowExpectedDisbursements(product.getDisallowExpectedDisbursements()).setFraud(acc.fraud)
-                .setOverpaidOnDate(acc.overpaidOnDate);
+                .setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff);
     }
 
     /*
@@ -654,7 +655,7 @@ public class LoanAccountData {
             final boolean canUseForTopup, final boolean isTopup, final Long closureLoanId, final String closureLoanAccountNo,
             final BigDecimal topupAmount, final boolean isEqualAmortization, final BigDecimal fixedPrincipalPercentagePerInstallment,
             final DelinquencyRangeData delinquencyRange, final boolean disallowExpectedDisbursements, final boolean fraud,
-            LocalDate lastClosedBusinessDate, LocalDate overpaidOnDate) {
+            LocalDate lastClosedBusinessDate, LocalDate overpaidOnDate, final boolean chargedOff) {
 
         final CollectionData delinquent = CollectionData.template();
 
@@ -694,7 +695,7 @@ public class LoanAccountData {
                 .setClosureLoanAccountNo(closureLoanAccountNo).setTopupAmount(topupAmount).setIsEqualAmortization(isEqualAmortization)
                 .setFixedPrincipalPercentagePerInstallment(fixedPrincipalPercentagePerInstallment).setDelinquent(delinquent)
                 .setDelinquencyRange(delinquencyRange).setDisallowExpectedDisbursements(disallowExpectedDisbursements).setFraud(fraud)
-                .setLastClosedBusinessDate(lastClosedBusinessDate).setOverpaidOnDate(overpaidOnDate);
+                .setLastClosedBusinessDate(lastClosedBusinessDate).setOverpaidOnDate(overpaidOnDate).setChargedOff(chargedOff);
     }
 
     /*
@@ -778,7 +779,8 @@ public class LoanAccountData {
                 .setIsEqualAmortization(acc.isEqualAmortization)
                 .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment)
                 .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
-                .setFraud(acc.fraud).setLastClosedBusinessDate(acc.getLastClosedBusinessDate()).setOverpaidOnDate(acc.overpaidOnDate);
+                .setFraud(acc.fraud).setLastClosedBusinessDate(acc.getLastClosedBusinessDate()).setOverpaidOnDate(acc.overpaidOnDate)
+                .setChargedOff(acc.chargedOff);
     }
 
     public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection<LoanProductData> productOptions,
@@ -853,7 +855,7 @@ public class LoanAccountData {
                 .setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
                 .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
                 .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
-                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate);
+                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff);
     }
 
     public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map<Long, Integer> memberLoanCycle) {
@@ -950,7 +952,7 @@ public class LoanAccountData {
                 .setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
                 .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
                 .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
-                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate);
+                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff);
     }
 
     public static LoanAccountData withInterestRecalculationCalendarData(final LoanAccountData acc, final CalendarData calendarData,
@@ -1015,7 +1017,7 @@ public class LoanAccountData {
                 .setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
                 .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
                 .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
-                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate);
+                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff);
     }
 
     public static LoanAccountData withLoanCalendarData(final LoanAccountData acc, final CalendarData calendarData) {
@@ -1073,7 +1075,7 @@ public class LoanAccountData {
                 .setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
                 .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
                 .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
-                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate);
+                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff);
     }
 
     public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) {
@@ -1134,7 +1136,7 @@ public class LoanAccountData {
                 .setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
                 .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
                 .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
-                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate);
+                .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff);
     }
 
     public static final Comparator<LoanAccountData> ClientNameComparator = (loan1, loan2) -> {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanApplicationTimelineData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanApplicationTimelineData.java
index 750d10e5e..3c960d174 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanApplicationTimelineData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanApplicationTimelineData.java
@@ -62,6 +62,11 @@ public class LoanApplicationTimelineData {
     private final String writeOffByFirstname;
     private final String writeOffByLastname;
 
+    private final LocalDate chargedOffOnDate;
+    private final String chargedOffByUsername;
+    private final String chargedOffByFirstname;
+    private final String chargedOffByLastname;
+
     public static LoanApplicationTimelineData templateDefault(final LocalDate expectedDisbursementDate) {
 
         final LocalDate submittedOnDate = null;
@@ -93,13 +98,18 @@ public class LoanApplicationTimelineData {
         final String writeOffByUsername = null;
         final String writeOffByFirstname = null;
         final String writeOffByLastname = null;
+        final LocalDate chargedOffOnDate = null;
+        final String chargedOffByUsername = null;
+        final String chargedOffByFirstname = null;
+        final String chargedOffByLastname = null;
 
         return new LoanApplicationTimelineData(submittedOnDate, submittedByUsername, submittedByFirstname, submittedByLastname,
                 rejectedOnDate, rejectedByUsername, rejectedByFirstname, rejectedByLastname, withdrawnOnDate, withdrawnByUsername,
                 withdrawnByFirstname, withdrawnByLastname, approvedOnDate, approvedByUsername, approvedByFirstname, approvedByLastname,
                 expectedDisbursementDate, actualDisbursementDate, disbursedByUsername, disbursedByFirstname, disbursedByLastname,
                 closedOnDate, closedByUsername, closedByFirstname, closedByLastname, expectedMaturityDate, writeOffOnDate,
-                writeOffByUsername, writeOffByFirstname, writeOffByLastname);
+                writeOffByUsername, writeOffByFirstname, writeOffByLastname, chargedOffOnDate, chargedOffByUsername, chargedOffByFirstname,
+                chargedOffByLastname);
     }
 
     public RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData(final CurrencyData currency, final BigDecimal principal,
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java
index 11114719d..9c141e606 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java
@@ -87,6 +87,8 @@ public class LoanSummaryData {
     private BigDecimal totalCreditBalanceRefundReversed;
     private BigDecimal totalRepaymentTransaction;
     private BigDecimal totalRepaymentTransactionReversed;
+    private final Long chargeOffReasonId;
+    private final String chargeOffReason;
 
     public LoanSummaryData(final CurrencyData currency, final BigDecimal principalDisbursed, final BigDecimal principalAdjustments,
             final BigDecimal principalPaid, final BigDecimal principalWrittenOff, final BigDecimal principalOutstanding,
@@ -100,7 +102,7 @@ public class LoanSummaryData {
             final BigDecimal totalRepayment, final BigDecimal totalExpectedCostOfLoan, final BigDecimal totalCostOfLoan,
             final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalOutstanding,
             final BigDecimal totalOverdue, final LocalDate overdueSinceDate, final Long writeoffReasonId, final String writeoffReason,
-            final BigDecimal totalRecovered) {
+            final BigDecimal totalRecovered, final Long chargeOffReasonId, final String chargeOffReason) {
         this.currency = currency;
         this.principalDisbursed = principalDisbursed;
         this.principalAdjustments = principalAdjustments;
@@ -139,6 +141,8 @@ public class LoanSummaryData {
         this.writeoffReasonId = writeoffReasonId;
         this.writeoffReason = writeoffReason;
         this.totalRecovered = totalRecovered;
+        this.chargeOffReasonId = chargeOffReasonId;
+        this.chargeOffReason = chargeOffReason;
     }
 
     public static LoanSummaryData withTransactionAmountsSummary(final LoanSummaryData defaultSummaryData,
@@ -193,12 +197,13 @@ public class LoanSummaryData {
                 defaultSummaryData.totalExpectedRepayment, defaultSummaryData.totalRepayment, defaultSummaryData.totalExpectedCostOfLoan,
                 defaultSummaryData.totalCostOfLoan, defaultSummaryData.totalWaived, defaultSummaryData.totalWrittenOff,
                 defaultSummaryData.totalOutstanding, defaultSummaryData.totalOverdue, defaultSummaryData.overdueSinceDate,
-                defaultSummaryData.writeoffReasonId, defaultSummaryData.writeoffReason, defaultSummaryData.totalRecovered)
-                        .setTotalMerchantRefund(totalMerchantRefund).setTotalMerchantRefundReversed(totalMerchantRefundReversed)
-                        .setTotalPayoutRefund(totalPayoutRefund).setTotalPayoutRefundReversed(totalPayoutRefundReversed)
-                        .setTotalGoodwillCredit(totalGoodwillCredit).setTotalGoodwillCreditReversed(totalGoodwillCreditReversed)
-                        .setTotalChargeAdjustment(totalChargeAdjustment).setTotalChargeAdjustmentReversed(totalChargeAdjustmentReversed)
-                        .setTotalChargeback(totalChargeback).setTotalCreditBalanceRefund(totalCreditBalanceRefund)
+                defaultSummaryData.writeoffReasonId, defaultSummaryData.writeoffReason, defaultSummaryData.totalRecovered,
+                defaultSummaryData.chargeOffReasonId, defaultSummaryData.chargeOffReason).setTotalMerchantRefund(totalMerchantRefund)
+                        .setTotalMerchantRefundReversed(totalMerchantRefundReversed).setTotalPayoutRefund(totalPayoutRefund)
+                        .setTotalPayoutRefundReversed(totalPayoutRefundReversed).setTotalGoodwillCredit(totalGoodwillCredit)
+                        .setTotalGoodwillCreditReversed(totalGoodwillCreditReversed).setTotalChargeAdjustment(totalChargeAdjustment)
+                        .setTotalChargeAdjustmentReversed(totalChargeAdjustmentReversed).setTotalChargeback(totalChargeback)
+                        .setTotalCreditBalanceRefund(totalCreditBalanceRefund)
                         .setTotalCreditBalanceRefundReversed(totalCreditBalanceRefundReversed)
                         .setTotalRepaymentTransaction(totalRepaymentTransaction)
                         .setTotalRepaymentTransactionReversed(totalRepaymentTransactionReversed);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java
index b18e12d09..dbd1e4d4c 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java
@@ -96,6 +96,8 @@ public class LoanTransactionData {
 
     private List<LoanTransactionRelationData> transactionRelations;
 
+    private Collection<CodeValueData> chargeOffReasonOptions = null;
+
     public static LoanTransactionData importInstance(BigDecimal repaymentAmount, LocalDate lastRepaymentDate, Long repaymentTypeId,
             Integer rowIndex, String locale, String dateFormat) {
         return new LoanTransactionData(repaymentAmount, lastRepaymentDate, repaymentTypeId, rowIndex, locale, dateFormat);
@@ -369,6 +371,10 @@ public class LoanTransactionData {
         this.writeOffReasonOptions = writeOffReasonOptions;
     }
 
+    public void setChargeOffReasonOptions(Collection<CodeValueData> chargeOffReasonOptions) {
+        this.chargeOffReasonOptions = chargeOffReasonOptions;
+    }
+
     public void setLoanChargePaidByList(Collection<LoanChargePaidByData> loanChargePaidByList) {
         this.loanChargePaidByList = loanChargePaidByList;
     }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index 8f3937e3b..8f13a73e2 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -439,6 +439,20 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
     @Column(name = "last_closed_business_date")
     private LocalDate lastClosedBusinessDate;
 
+    @Column(name = "is_charged_off", nullable = false)
+    private boolean chargedOff;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "charge_off_reason_cv_id")
+    private CodeValue chargeOffReason;
+
+    @Column(name = "charged_off_on_date")
+    private LocalDate chargedOffOnDate;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "charged_off_by_userid")
+    private AppUser chargedOffBy;
+
     public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType,
             final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
             final String transactionProcessingStrategyCode, final LoanProductRelatedDetail loanRepaymentScheduleDetail,
@@ -6949,4 +6963,23 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
     public void setLastClosedBusinessDate(LocalDate lastClosedBusinessDate) {
         this.lastClosedBusinessDate = lastClosedBusinessDate;
     }
+
+    public void markAsChargedOff(final LocalDate chargedOffOn, final AppUser chargedOffBy, final CodeValue chargeOffReason) {
+        this.chargedOff = true;
+        this.chargedOffBy = chargedOffBy;
+        this.chargedOffOnDate = chargedOffOn;
+        this.chargeOffReason = chargeOffReason;
+    }
+
+    public void liftChargeOff() {
+        this.chargedOff = false;
+        this.chargedOffBy = null;
+        this.chargedOffOnDate = null;
+        this.chargeOffReason = null;
+    }
+
+    public boolean isChargedOff() {
+        return this.chargedOff;
+    }
+
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index 5e348d2b0..c95fd3710 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -347,6 +347,25 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
         return new LoanTransaction(loan, office, LoanTransactionType.WRITEOFF, null, writeOffDate, externalId);
     }
 
+    public static LoanTransaction chargeOff(final Loan loan, final LocalDate chargeOffDate, final ExternalId externalId) {
+        BigDecimal principalPortion = loan.getLoanSummary().getTotalPrincipalOutstanding().compareTo(BigDecimal.ZERO) != 0
+                ? loan.getLoanSummary().getTotalPrincipalOutstanding()
+                : null;
+        BigDecimal interestPortion = loan.getLoanSummary().getTotalInterestOutstanding().compareTo(BigDecimal.ZERO) != 0
+                ? loan.getLoanSummary().getTotalInterestOutstanding()
+                : null;
+        BigDecimal feePortion = loan.getLoanSummary().getTotalFeeChargesOutstanding().compareTo(BigDecimal.ZERO) != 0
+                ? loan.getLoanSummary().getTotalFeeChargesOutstanding()
+                : null;
+        BigDecimal penaltyPortion = loan.getLoanSummary().getTotalPenaltyChargesOutstanding().compareTo(BigDecimal.ZERO) != 0
+                ? loan.getLoanSummary().getTotalPenaltyChargesOutstanding()
+                : null;
+        BigDecimal totalOutstanding = loan.getLoanSummary().getTotalOutstanding();
+
+        return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGE_OFF.getValue(), chargeOffDate, totalOutstanding,
+                principalPortion, interestPortion, feePortion, penaltyPortion, null, false, null, externalId);
+    }
+
     private LoanTransaction(final Loan loan, final Office office, final LoanTransactionType type, final BigDecimal amount,
             final LocalDate date, final ExternalId externalId) {
         this.loan = loan;
@@ -647,6 +666,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
         return getTypeOf().isWriteOff() && isNotReversed();
     }
 
+    public boolean isChargeOff() {
+        return getTypeOf().isChargeOff() && isNotReversed();
+    }
+
     public boolean isIdentifiedBy(final Long identifier) {
         return getId().equals(identifier);
     }
@@ -786,7 +809,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
         return isNotReversed() && (LoanTransactionType.CONTRA.equals(getTypeOf())
                 || LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf())
                 || LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf()) || LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf())
-                || LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) || LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf()));
+                || LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) || LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf())
+                || LoanTransactionType.CHARGE_OFF.equals(getTypeOf()));
     }
 
     public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
index b510f91d9..5ceaf0278 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
@@ -58,7 +58,8 @@ public enum LoanTransactionType {
     GOODWILL_CREDIT(23, "loanTransactionType.goodwillCredit"), //
     CHARGE_REFUND(24, "loanTransactionType.chargeRefund"), //
     CHARGEBACK(25, "loanTransactionType.chargeback"), //
-    CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment");
+    CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment"), //
+    CHARGE_OFF(27, "loanTransactionType.chargeOff");
 
     private final Integer value;
     private final String code;
@@ -100,6 +101,7 @@ public enum LoanTransactionType {
             case 24 -> LoanTransactionType.CHARGE_REFUND;
             case 25 -> LoanTransactionType.CHARGEBACK;
             case 26 -> LoanTransactionType.CHARGE_ADJUSTMENT;
+            case 27 -> LoanTransactionType.CHARGE_OFF;
             default -> LoanTransactionType.INVALID;
         };
     }
@@ -184,4 +186,7 @@ public enum LoanTransactionType {
         return this.equals(LoanTransactionType.CHARGE_ADJUSTMENT);
     }
 
+    public boolean isChargeOff() {
+        return this.equals(LoanTransactionType.CHARGE_OFF);
+    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ChargeOffLoanCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ChargeOffLoanCommandHandler.java
new file mode 100644
index 000000000..acf640f21
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ChargeOffLoanCommandHandler.java
@@ -0,0 +1,43 @@
+/**
+ * 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.portfolio.loanaccount.handler;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "LOAN", action = "CHARGEOFF")
+public class ChargeOffLoanCommandHandler implements NewCommandSourceHandler {
+
+    private final LoanWritePlatformService writePlatformService;
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+
+        return writePlatformService.chargeOff(command);
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java
index f39d5cdad..088465ac8 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java
@@ -341,6 +341,36 @@ public final class LoanEventApiJsonValidator {
         throwExceptionIfValidationWarningsExist(dataValidationErrors);
     }
 
+    public void validateChargeOffTransaction(final String json) {
+        if (StringUtils.isBlank(json)) {
+            throw new InvalidJsonException();
+        }
+
+        final Set<String> chargeOffParameters = new HashSet<>(
+                Arrays.asList("transactionDate", "note", "locale", "dateFormat", "chargeOffReasonId", "externalId"));
+
+        final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
+        fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, chargeOffParameters);
+
+        final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+        final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
+
+        final JsonElement element = fromApiJsonHelper.parse(json);
+        final LocalDate transactionDate = fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
+        baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
+
+        final String note = fromApiJsonHelper.extractStringNamed("note", element);
+        baseDataValidator.reset().parameter("note").value(note).ignoreIfNull().notExceedingLengthOf(1000);
+
+        final String externalId = fromApiJsonHelper.extractStringNamed("externalId", element);
+        baseDataValidator.reset().parameter("externalId").value(externalId).ignoreIfNull().notExceedingLengthOf(100);
+
+        final Long chargeOffReasonId = fromApiJsonHelper.extractLongNamed("chargeOffReasonId", element);
+        baseDataValidator.reset().parameter("chargeOffReasonId").value(chargeOffReasonId).ignoreIfNull().integerGreaterThanZero();
+
+        throwExceptionIfValidationWarningsExist(dataValidationErrors);
+    }
+
     public void validateUpdateOfLoanOfficer(final String json) {
         if (StringUtils.isBlank(json)) {
             throw new InvalidJsonException();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
index 12c0eb587..220141156 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
@@ -116,6 +116,8 @@ public interface LoanReadPlatformService {
 
     Collection<LoanScheduleAccrualData> retrievePeriodicAccrualData(LocalDate tillDate);
 
+    LoanTransactionData retrieveLoanChargeOffTemplate(Long loanId);
+
     Collection<Long> fetchLoansForInterestRecalculation();
 
     List<Long> fetchLoansForInterestRecalculation(Integer pageSize, Long maxLoanIdInList, String officeHierarchy);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
index b9f7edba2..7b587c043 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
@@ -677,7 +677,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
                     + " lpvi.minimum_gap as minimuminstallmentgap, lpvi.maximum_gap as maximuminstallmentgap, "
                     + " lp.can_use_for_topup as canUseForTopup, l.is_topup as isTopup, topup.closure_loan_id as closureLoanId, "
                     + " l.total_recovered_derived as totalRecovered, topuploan.account_no as closureLoanAccountNo, "
-                    + " topup.topup_amount as topupAmount, l.last_closed_business_date as lastClosedBusinessDate,l.overpaidon_date as overpaidOnDate from m_loan l" //
+                    + " topup.topup_amount as topupAmount, l.last_closed_business_date as lastClosedBusinessDate,l.overpaidon_date as overpaidOnDate, "
+                    + " l.is_charged_off as isChargedOff, l.charge_off_reason_cv_id as chargeOffReasonId, codec.code_value as chargeOffReason, l.charged_off_on_date as chargedOffOnDate, "
+                    + " cobu.username as chargedOffByUsername, cobu.firstname as chargedOffByFirstname, cobu.lastname as chargedOffByLastname "
+                    + " from m_loan l" //
                     + " join m_product_loan lp on lp.id = l.product_id" //
                     + " left join m_loan_recalculation_details lir on lir.loan_id = l.id join m_currency rc on rc."
                     + sqlGenerator.escape("code") + " = l.currency_code" //
@@ -690,8 +693,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
                     + " left join m_appuser wbu on wbu.id = l.withdrawnon_userid"
                     + " left join m_appuser abu on abu.id = l.approvedon_userid"
                     + " left join m_appuser dbu on dbu.id = l.disbursedon_userid left join m_appuser cbu on cbu.id = l.closedon_userid"
+                    + " left join m_appuser cobu on cobu.id = l.charged_off_by_userid "
                     + " left join m_code_value cv on cv.id = l.loanpurpose_cv_id"
                     + " left join m_code_value codev on codev.id = l.writeoff_reason_cv_id"
+                    + " left join m_code_value codec on codec.id = l.charge_off_reason_cv_id"
                     + " left join m_product_loan_variable_installment_config lpvi on lpvi.loan_product_id = l.product_id"
                     + " left join m_loan_topup as topup on l.id = topup.loan_id"
                     + " left join m_loan as topuploan on topuploan.id = topup.closure_loan_id ";
@@ -792,12 +797,21 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
             final Integer minimumGap = rs.getInt("minimuminstallmentgap");
             final Integer maximumGap = rs.getInt("maximuminstallmentgap");
 
+            final LocalDate chargedOffOnDate = JdbcSupport.getLocalDate(rs, "chargedOffOnDate");
+            final String chargedOffByUsername = rs.getString("chargedOffByUsername");
+            final String chargedOffByFirstname = rs.getString("chargedOffByFirstname");
+            final String chargedOffByLastname = rs.getString("chargedOffByLastname");
+            final Long chargeOffReasonId = JdbcSupport.getLong(rs, "chargeOffReasonId");
+            final String chargeOffReason = rs.getString("chargeOffReason");
+            final boolean isChargedOff = rs.getBoolean("isChargedOff");
+
             final LoanApplicationTimelineData timeline = new LoanApplicationTimelineData(submittedOnDate, submittedByUsername,
                     submittedByFirstname, submittedByLastname, rejectedOnDate, rejectedByUsername, rejectedByFirstname, rejectedByLastname,
                     withdrawnOnDate, withdrawnByUsername, withdrawnByFirstname, withdrawnByLastname, approvedOnDate, approvedByUsername,
                     approvedByFirstname, approvedByLastname, expectedDisbursementDate, actualDisbursementDate, disbursedByUsername,
                     disbursedByFirstname, disbursedByLastname, closedOnDate, closedByUsername, closedByFirstname, closedByLastname,
-                    expectedMaturityDate, writtenOffOnDate, closedByUsername, closedByFirstname, closedByLastname);
+                    expectedMaturityDate, writtenOffOnDate, closedByUsername, closedByFirstname, closedByLastname, chargedOffOnDate,
+                    chargedOffByUsername, chargedOffByFirstname, chargedOffByLastname);
 
             final BigDecimal principal = rs.getBigDecimal("principal");
             final BigDecimal approvedPrincipal = rs.getBigDecimal("approvedPrincipal");
@@ -914,7 +928,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
                         penaltyChargesCharged, penaltyChargesPaid, penaltyChargesWaived, penaltyChargesWrittenOff,
                         penaltyChargesOutstanding, penaltyChargesOverdue, totalExpectedRepayment, totalRepayment, totalExpectedCostOfLoan,
                         totalCostOfLoan, totalWaived, totalWrittenOff, totalOutstanding, totalOverdue, overdueSinceDate, writeoffReasonId,
-                        writeoffReason, totalRecovered);
+                        writeoffReason, totalRecovered, chargeOffReasonId, chargeOffReason);
             }
 
             GroupGeneralData groupData = null;
@@ -1022,7 +1036,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
                     createStandingInstructionAtDisbursement, isvariableInstallmentsAllowed, minimumGap, maximumGap, loanSubStatus,
                     canUseForTopup, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization,
                     fixedPrincipalPercentagePerInstallment, delinquencyRange, disallowExpectedDisbursements, isFraud,
-                    lastClosedBusinessDate, overpaidOnDate);
+                    lastClosedBusinessDate, overpaidOnDate, isChargedOff);
         }
     }
 
@@ -1917,6 +1931,26 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
         return loanTransactionData;
     }
 
+    @Override
+    public LoanTransactionData retrieveLoanChargeOffTemplate(final Long loanId) {
+
+        final LoanAccountData loan = this.retrieveOne(loanId);
+        final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.CHARGE_OFF);
+        final BigDecimal totalOutstanding = loan.getSummary() != null ? loan.getSummary().getTotalOutstanding() : null;
+        final BigDecimal totalPrincipalOutstanding = loan.getSummary() != null ? loan.getSummary().getPrincipalOutstanding() : null;
+        final BigDecimal totalInterestOutstanding = loan.getSummary() != null ? loan.getSummary().getInterestOutstanding() : null;
+        final BigDecimal totalFeeOutstanding = loan.getSummary() != null ? loan.getSummary().getFeeChargesOutstanding() : null;
+        final BigDecimal totalPenaltyOutstanding = loan.getSummary() != null ? loan.getSummary().getPenaltyChargesOutstanding() : null;
+        final List<CodeValueData> chargeOffReasonOptions = new ArrayList<>(
+                this.codeValueReadPlatformService.retrieveCodeValuesByCode(LoanApiConstants.CHARGE_OFF_REASONS));
+        LoanTransactionData loanTransactionData = new LoanTransactionData(null, null, null, transactionType, null, loan.getCurrency(),
+                DateUtils.getBusinessLocalDate(), totalOutstanding, loan.getNetDisbursalAmount(), totalPrincipalOutstanding,
+                totalInterestOutstanding, totalFeeOutstanding, totalPenaltyOutstanding, null, ExternalId.empty(), null, null, null, null,
+                false, loanId, loan.getExternalId());
+        loanTransactionData.setChargeOffReasonOptions(chargeOffReasonOptions);
+        return loanTransactionData;
+    }
+
     @Override
     public Collection<Long> fetchLoansForInterestRecalculation() {
         StringBuilder sqlBuilder = new StringBuilder();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java
index f69352caf..41be2c0ac 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java
@@ -111,4 +111,5 @@ public interface LoanWritePlatformService {
 
     CommandProcessingResult markLoanAsFraud(Long loanId, JsonCommand command);
 
+    CommandProcessingResult chargeOff(JsonCommand command);
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
index 67cf466ca..5ec677d5f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
@@ -2644,6 +2644,58 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
                 .build();
     }
 
+    @Override
+    @Transactional
+    public CommandProcessingResult chargeOff(JsonCommand command) {
+
+        loanEventApiJsonValidator.validateChargeOffTransaction(command.json());
+
+        final Map<String, Object> changes = new LinkedHashMap<>();
+        changes.put(LoanApiConstants.transactionDateParamName,
+                command.stringValueOfParameterNamed(LoanApiConstants.transactionDateParamName));
+        changes.put(LoanApiConstants.localeParameterName, command.locale());
+        changes.put(LoanApiConstants.dateFormatParameterName, command.dateFormat());
+
+        // TODO: add business logic validation (transaction date cannot be future, cannot be earlier than latest
+        // transactions, etc)
+        Loan loan = loanAssembler.assembleFrom(command.getLoanId());
+        final LocalDate transactionDate = command.localDateValueOfParameterNamed(LoanApiConstants.transactionDateParamName);
+        final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
+        final AppUser currentUser = getAppUserIfPresent();
+
+        if (command.hasParameter(LoanApiConstants.chargeOffReasonIdParamName)) {
+            Long chargeOffReasonId = command.longValueOfParameterNamed(LoanApiConstants.chargeOffReasonIdParamName);
+            CodeValue chargeOffReason = this.codeValueRepository
+                    .findOneByCodeNameAndIdWithNotFoundDetection(LoanApiConstants.CHARGE_OFF_REASONS, chargeOffReasonId);
+            changes.put(LoanApiConstants.chargeOffReasonIdParamName, chargeOffReasonId);
+            loan.markAsChargedOff(transactionDate, currentUser, chargeOffReason);
+        } else {
+            loan.markAsChargedOff(transactionDate, currentUser, null);
+        }
+
+        LoanTransaction chargeOffTransaction = LoanTransaction.chargeOff(loan, transactionDate, txnExternalId);
+        loanTransactionRepository.saveAndFlush(chargeOffTransaction);
+
+        String noteText = command.stringValueOfParameterNamed(LoanApiConstants.noteParameterName);
+        if (StringUtils.isNotBlank(noteText)) {
+            changes.put(LoanApiConstants.noteParameterName, noteText);
+            final Note note = Note.loanTransactionNote(loan, chargeOffTransaction, noteText);
+            this.noteRepository.save(note);
+        }
+
+        // TODO: add accounting
+        // TODO: add external events
+        return new CommandProcessingResultBuilder() //
+                .withCommandId(command.commandId()) //
+                .withEntityId(chargeOffTransaction.getId()) //
+                .withEntityExternalId(chargeOffTransaction.getExternalId()) //
+                .withOfficeId(loan.getOfficeId()) //
+                .withClientId(loan.getClientId()) //
+                .withGroupId(loan.getGroupId()) //
+                .withLoanId(command.getLoanId()) //
+                .with(changes).build();
+    }
+
     private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) {
         if (!loan.isMultiDisburmentLoan()) {
             final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal";
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
index 972e3f49c..b6db1df1e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
@@ -291,6 +291,8 @@ public final class LoanEnumerations {
                     LoanTransactionType.CHARGEBACK.getCode(), "Chargeback");
             case CHARGE_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.CHARGE_ADJUSTMENT.getValue().longValue(),
                     LoanTransactionType.CHARGE_ADJUSTMENT.getCode(), "Charge Adjustment");
+            case CHARGE_OFF -> new LoanTransactionEnumData(LoanTransactionType.CHARGE_OFF.getValue().longValue(),
+                    LoanTransactionType.CHARGE_OFF.getCode(), "Charge-off");
         };
     }
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/loanaccount/api/SelfLoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/loanaccount/api/SelfLoansApiResourceSwagger.java
index 5f988beb5..c69337b1c 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/loanaccount/api/SelfLoansApiResourceSwagger.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/loanaccount/api/SelfLoansApiResourceSwagger.java
@@ -165,9 +165,9 @@ final class SelfLoansApiResourceSwagger {
             public String description;
         }
 
-        static final class GetLoansLoanIdTimeline {
+        static final class GetSelfLoanIdTimeline {
 
-            private GetLoansLoanIdTimeline() {}
+            private GetSelfLoanIdTimeline() {}
 
             @Schema(example = "[2012, 4, 3]")
             public LocalDate submittedOnDate;
@@ -201,9 +201,9 @@ final class SelfLoansApiResourceSwagger {
             public LocalDate closedOnDate;
         }
 
-        static final class GetLoansLoanIdSummary {
+        static final class GetSelfLoanIdSummary {
 
-            private GetLoansLoanIdSummary() {}
+            private GetSelfLoanIdSummary() {}
 
             static final class GetLoansLoanIdEmiVariations {
 
@@ -510,8 +510,8 @@ final class SelfLoansApiResourceSwagger {
         public GetLoansLoanIdInterestCalculationPeriodType interestCalculationPeriodType;
         @Schema(example = "mifos-standard-strategy")
         public String transactionProcessingStrategyCode;
-        public GetLoansLoanIdTimeline timeline;
-        public GetLoansLoanIdSummary summary;
+        public GetSelfLoanIdTimeline timeline;
+        public GetSelfLoanIdSummary summary;
     }
 
     @Schema(description = "GetSelfLoansLoanIdTransactionsTransactionIdResponse")
@@ -554,7 +554,7 @@ final class SelfLoansApiResourceSwagger {
         public LocalDate date;
         @Schema(example = "false")
         public Boolean manuallyReversed;
-        public GetSelfLoansLoanIdResponse.GetLoansLoanIdSummary.GetLoansLoanIdOverdueCharges.GetLoanCurrency currency;
+        public GetSelfLoansLoanIdResponse.GetSelfLoanIdSummary.GetLoansLoanIdOverdueCharges.GetLoanCurrency currency;
         @Schema(example = "559.88")
         public Float amount;
         @Schema(example = "559.88")
@@ -602,7 +602,7 @@ final class SelfLoansApiResourceSwagger {
         public Double percentage;
         @Schema(example = "0")
         public Double amountPercentageAppliedTo;
-        public GetSelfLoansLoanIdResponse.GetLoansLoanIdSummary.GetLoansLoanIdOverdueCharges.GetLoanCurrency currency;
+        public GetSelfLoansLoanIdResponse.GetSelfLoanIdSummary.GetLoansLoanIdOverdueCharges.GetLoanCurrency currency;
         @Schema(example = "100")
         public Float amount;
         @Schema(example = "0")
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 b29d9e2b9..ed63ba0e3 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
@@ -98,4 +98,5 @@
     <include file="parts/0076_add_loan_transaction_enum_values.xml" relativeToChangelogFile="true" />
     <include file="parts/0077_add_overpaid_date_for_loan.xml" relativeToChangelogFile="true" />
     <include file="parts/0078_add_configuration_cob_bulk_event.xml" relativeToChangelogFile="true" />
+    <include file="parts/0079_add_charge_off_details_to_loan.xml" relativeToChangelogFile="true" />
 </databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0079_add_charge_off_details_to_loan.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0079_add_charge_off_details_to_loan.xml
new file mode 100644
index 000000000..8fb616f34
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0079_add_charge_off_details_to_loan.xml
@@ -0,0 +1,50 @@
+<?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 author="fineract" id="1">
+        <insert tableName="m_code">
+            <column name="code_name" value="ChargeOffReasons"/>
+            <column name="is_system_defined" valueBoolean="true"/>
+        </insert>
+    </changeSet>
+    <changeSet author="fineract" id="2">
+        <addColumn tableName="m_loan">
+            <column defaultValueBoolean="false" name="is_charged_off" type="boolean">
+                <constraints nullable="false"/>
+            </column>
+            <column defaultValueComputed="NULL" name="charged_off_on_date" type="date"/>
+            <column defaultValueComputed="NULL" name="charge_off_reason_cv_id" type="BIGINT"/>
+            <column defaultValueComputed="NULL" name="charged_off_by_userid" type="BIGINT"/>
+        </addColumn>
+    </changeSet>
+    <changeSet id="3" author="fineract">
+        <insert tableName="m_permission">
+            <column name="grouping" value="transaction_loan"/>
+            <column name="code" value="CHARGEOFF_LOAN"/>
+            <column name="entity_name" value="LOAN"/>
+            <column name="action_name" value="CHARGEOFF"/>
+            <column name="can_maker_checker" valueBoolean="false"/>
+        </insert>
+    </changeSet>
+</databaseChangeLog>
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
index 2bce7d241..039dde92d 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
@@ -19,7 +19,9 @@
 package org.apache.fineract.integrationtests;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -89,6 +91,7 @@ import org.apache.fineract.integrationtests.common.savings.AccountTransferHelper
 import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
 import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
 import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker;
+import org.apache.fineract.integrationtests.common.system.CodeHelper;
 import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
 import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
 import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
@@ -6939,6 +6942,58 @@ public class ClientLoanIntegrationTest {
         assertEquals(0.0f, loanSummary.get("totalWaived"));
     }
 
+    @Test
+    public void chargeOff() {
+        final Account assetAccount = this.accountHelper.createAssetAccount();
+        final Account incomeAccount = this.accountHelper.createIncomeAccount();
+        final Account expenseAccount = this.accountHelper.createExpenseAccount();
+        final Account overpaymentAccount = this.accountHelper.createLiabilityAccount();
+        String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5);
+        Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
+        final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount, expenseAccount,
+                overpaymentAccount);
+
+        final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2011");
+
+        final Integer loanID = applyForLoanApplication(clientID, loanProductID);
+
+        HashMap<String, Object> loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID);
+        LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+        loanStatusHashMap = this.loanTransactionHelper.approveLoan("02 September 2022", loanID);
+        LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+        LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
+
+        loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September 2022", loanID, "1000");
+        LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+
+        GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
+        assertTrue(loanDetails.getStatus().getActive());
+        assertEquals(1000.0, loanDetails.getSummary().getTotalOutstanding());
+        assertFalse(loanDetails.getChargedOff());
+        assertNull(loanDetails.getSummary().getChargeOffReasonId());
+        assertNull(loanDetails.getSummary().getChargeOffReason());
+        assertNull(loanDetails.getTimeline().getChargedOffOnDate());
+        assertNull(loanDetails.getTimeline().getChargedOffByUsername());
+        assertNull(loanDetails.getTimeline().getChargedOffByFirstname());
+        assertNull(loanDetails.getTimeline().getChargedOffByLastname());
+
+        String transactionExternalId = UUID.randomUUID().toString();
+        this.loanTransactionHelper.chargeOffLoan((long) loanID, new PostLoansLoanIdTransactionsRequest().transactionDate("4 September 2022")
+                .locale("en").dateFormat("dd MMMM yyyy").externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
+
+        loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
+        assertTrue(loanDetails.getStatus().getActive());
+        assertEquals(1000.0, loanDetails.getSummary().getTotalOutstanding());
+        assertTrue(loanDetails.getChargedOff());
+        assertEquals((long) chargeOffReasonId, loanDetails.getSummary().getChargeOffReasonId());
+        assertEquals(randomText, loanDetails.getSummary().getChargeOffReason());
+        assertEquals(LocalDate.of(2022, 9, 4), loanDetails.getTimeline().getChargedOffOnDate());
+        assertEquals("mifos", loanDetails.getTimeline().getChargedOffByUsername());
+        assertEquals("App", loanDetails.getTimeline().getChargedOffByFirstname());
+        assertEquals("Administrator", loanDetails.getTimeline().getChargedOffByLastname());
+    }
+
     private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID) {
         LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
         final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1")
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalIdSupportIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalIdSupportIntegrationTest.java
index 203f99e38..aca122f19 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalIdSupportIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalIdSupportIntegrationTest.java
@@ -188,6 +188,7 @@ public class ExternalIdSupportIntegrationTest extends IntegrationTest {
         loanTransactionHelper.retrieveTransactionTemplate(loanExternalIdStr, "refundbycash", null, null, null);
         loanTransactionHelper.retrieveTransactionTemplate(loanExternalIdStr, "foreclosure", null, null, null);
         loanTransactionHelper.retrieveTransactionTemplate(loanExternalIdStr, "creditBalanceRefund", null, null, null);
+        loanTransactionHelper.retrieveTransactionTemplate(loanExternalIdStr, "charge-off", null, null, null);
 
         // Check whether an external id was generated
         String waiveChargeExternalIdStr = UUID.randomUUID().toString();
@@ -1069,6 +1070,23 @@ public class ExternalIdSupportIntegrationTest extends IntegrationTest {
                             .externalId(transactionExternalId2));
             assertEquals(transactionExternalId2, forecloseResult.getResourceExternalId());
 
+            String loanExternalIdStr16 = UUID.randomUUID().toString();
+            String transactionExternalId3 = UUID.randomUUID().toString();
+            applyForLoanApplication(client.getClientId().intValue(), loanProductID, loanExternalIdStr16);
+            this.loanTransactionHelper.approveLoan(loanExternalIdStr16,
+                    new PostLoansLoanIdRequest().approvedOnDate("2 September 2022").approvedLoanAmount(new BigDecimal("1000"))
+                            .expectedDisbursementDate("2 September 2022").locale("en").dateFormat("dd MMMM yyyy"));
+            this.loanTransactionHelper.disburseLoan(loanExternalIdStr16,
+                    new PostLoansLoanIdRequest().actualDisbursementDate("2 September 2022").transactionAmount(new BigDecimal("1000"))
+                            .locale("en").dateFormat("dd MMMM yyyy"));
+            this.loanTransactionHelper.disburseLoan(loanExternalIdStr16,
+                    new PostLoansLoanIdRequest().actualDisbursementDate("2 September 2022").transactionAmount(new BigDecimal("1000"))
+                            .locale("en").dateFormat("dd MMMM yyyy"));
+            PostLoansLoanIdTransactionsResponse chargeOffResult = this.loanTransactionHelper.chargeOffLoan(loanExternalIdStr16,
+                    new PostLoansLoanIdTransactionsRequest().transactionDate("2 September 2022").locale("en").dateFormat("dd MMMM yyyy")
+                            .externalId(transactionExternalId3));
+            assertEquals(transactionExternalId3, chargeOffResult.getResourceExternalId());
+
         } finally {
             GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
             GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 50, false);
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index bd22b2307..c2ee76342 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -1719,4 +1719,12 @@ public class LoanTransactionHelper extends IntegrationTest {
     public PostLoansLoanIdTransactionsResponse forecloseLoan(String loanExternalId, PostLoansLoanIdTransactionsRequest request) {
         return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "foreclosure"));
     }
+
+    public PostLoansLoanIdTransactionsResponse chargeOffLoan(String loanExternalId, PostLoansLoanIdTransactionsRequest request) {
+        return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "charge-off"));
+    }
+
+    public PostLoansLoanIdTransactionsResponse chargeOffLoan(Long loanId, PostLoansLoanIdTransactionsRequest request) {
+        return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "charge-off"));
+    }
 }
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java
index 507b8c7d7..c1542ed76 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java
@@ -34,6 +34,7 @@ public final class CodeHelper {
     private static final String COUNTRY_CODE_NAME = "COUNTRY";
     private static final String STATE_CODE_NAME = "STATE";
     private static final String ADDRESS_TYPE_CODE_NAME = "ADDRESS_TYPE";
+    private static final String CHARGE_OFF_REASONS_CODE_NAME = "ChargeOffReasons";
 
     private CodeHelper() {
 
@@ -175,6 +176,13 @@ public final class CodeHelper {
         return createCodeValue(requestSpec, responseSpec, countryCode, countryName, position);
     }
 
+    public static Integer createChargeOffCodeValue(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
+            final String value, final Integer position) {
+        HashMap<String, Object> code = getCodeByName(requestSpec, responseSpec, CHARGE_OFF_REASONS_CODE_NAME);
+        Integer countryCode = (Integer) code.get("id");
+        return createCodeValue(requestSpec, responseSpec, countryCode, value, position);
+    }
+
     public static Integer createCodeValue(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
             final Integer codeId, final String codeValueName, final Integer position) {
         return (Integer) createCodeValue(requestSpec, responseSpec, codeId, codeValueName, position, SUBRESPONSE_ID_ATTRIBUTE_NAME);