From c7504680b76c55d685d12d1a6edf995544881c7c Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Thu, 5 Jan 2023 09:36:40 +0100 Subject: [PATCH] FINERACT-1806: Charge-off API --- .../main/avro/loan/v1/LoanAccountDataV1.avsc | 24 ++++++++ .../v1/LoanApplicationTimelineDataV1.avsc | 32 +++++++++++ .../main/avro/loan/v1/LoanSummaryDataV1.avsc | 16 ++++++ .../service/CommandWrapperBuilder.java | 8 +++ ...sReadPlatformServiceJpaRepositoryImpl.java | 17 ++++-- .../loanaccount/api/LoanApiConstants.java | 4 +- .../api/LoanTransactionsApiResource.java | 15 ++++- .../LoanTransactionsApiResourceSwagger.java | 5 +- .../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 ++++++++- .../domain/LoanTransactionType.java | 7 ++- .../handler/ChargeOffLoanCommandHandler.java | 43 +++++++++++++++ .../LoanEventApiJsonValidator.java | 30 ++++++++++ .../service/LoanReadPlatformService.java | 2 + .../service/LoanReadPlatformServiceImpl.java | 42 ++++++++++++-- .../service/LoanWritePlatformService.java | 1 + ...WritePlatformServiceJpaRepositoryImpl.java | 52 ++++++++++++++++++ .../loanproduct/service/LoanEnumerations.java | 2 + .../api/SelfLoansApiResourceSwagger.java | 16 +++--- .../db/changelog/tenant/changelog-tenant.xml | 1 + .../0079_add_charge_off_details_to_loan.xml | 50 +++++++++++++++++ .../ClientLoanIntegrationTest.java | 55 +++++++++++++++++++ .../ExternalIdSupportIntegrationTest.java | 18 ++++++ .../common/loans/LoanTransactionHelper.java | 8 +++ .../common/system/CodeHelper.java | 8 +++ 30 files changed, 549 insertions(+), 43 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ChargeOffLoanCommandHandler.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0079_add_charge_off_details_to_loan.xml 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 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 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 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 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 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 transactionRelations; + private Collection 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 chargeOffReasonOptions) { + this.chargeOffReasonOptions = chargeOffReasonOptions; + } + public void setLoanChargePaidByList(Collection 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 chargeOffParameters = new HashSet<>( + Arrays.asList("transactionDate", "note", "locale", "dateFormat", "chargeOffReasonId", "externalId")); + + final Type typeOfMap = new TypeToken>() {}.getType(); + fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, chargeOffParameters); + + final List 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 retrievePeriodicAccrualData(LocalDate tillDate); + LoanTransactionData retrieveLoanChargeOffTemplate(Long loanId); + Collection fetchLoansForInterestRecalculation(); List 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 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 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 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 @@ + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 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 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);