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