From 91dbe491b6ad158ac1d4975f3203a83891f32e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soma=20S=C3=B6r=C3=B6s?= Date: Thu, 4 Jun 2026 14:35:42 +0200 Subject: [PATCH] FINERACT-2455: Working Capital Loan Transaction Undo - Generic --- .../domain/CommandWrapperConstants.java | 1 + .../service/CommandWrapperBuilder.java | 10 ++ ...ingCapitalAmortizationScheduleStepDef.java | 13 ++- .../WorkingCapitalLoanAccountStepDef.java | 25 ++++ .../WorkingCapitalBreachEvaluation.feature | 7 +- .../WorkingCapitalLoanUndoTransaction.feature | 110 ++++++++++++++++++ .../WorkingCapitalLoanConstants.java | 6 + ...InternalWorkingCapitalLoanApiResource.java | 20 +++- ...ingCapitalLoanTransactionsApiResource.java | 102 ++++++++++++++-- ...talLoanTransactionsApiResourceSwagger.java | 28 +++++ .../ProjectedAmortizationScheduleModel.java | 15 +++ .../domain/WorkingCapitalLoanEvent.java | 1 + ...rkingCapitalLoanLifecycleStateMachine.java | 1 + ...gCapitalLoanTransactionCommandHandler.java | 42 +++++++ ...oanDelinquencyRangeScheduleRepository.java | 2 + .../WorkingCapitalLoanDataValidator.java | 28 +++++ ...ernalWorkingCapitalLoanPaymentService.java | 27 ----- ...lWorkingCapitalLoanPaymentServiceImpl.java | 57 --------- ...lLoanAmortizationScheduleWriteService.java | 2 + ...nAmortizationScheduleWriteServiceImpl.java | 17 +++ ...rkingCapitalLoanBreachScheduleService.java | 2 + ...gCapitalLoanBreachScheduleServiceImpl.java | 23 ++++ ...lLoanDelinquencyClassificationService.java | 6 +- ...nDelinquencyClassificationServiceImpl.java | 17 +++ ...alLoanDelinquencyRangeScheduleService.java | 1 + ...anDelinquencyRangeScheduleServiceImpl.java | 38 ++++++ ...orkingCapitalLoanWritePlatformService.java | 2 + ...ngCapitalLoanWritePlatformServiceImpl.java | 102 +++++++++++++++- 28 files changed, 598 insertions(+), 107 deletions(-) create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanUndoTransaction.feature create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/UndoWorkingCapitalLoanTransactionCommandHandler.java delete mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentService.java delete mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentServiceImpl.java diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapperConstants.java b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapperConstants.java index be71670437b..26b23465897 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapperConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandWrapperConstants.java @@ -183,6 +183,7 @@ private CommandWrapperConstants() {} public static final String ENTITY_LOANPRODUCT = "LOANPRODUCT"; public static final String ENTITY_WORKINGCAPITALLOANPRODUCT = "WORKINGCAPITALLOANPRODUCT"; public static final String ENTITY_WORKINGCAPITALLOAN = "WORKINGCAPITALLOAN"; + public static final String ENTITY_WORKINGCAPITALLOANTRANSACTION = "ENTITY_WORKINGCAPITALLOANTRANSACTION"; public static final String ENTITY_CLIENTIDENTIFIER = "CLIENTIDENTIFIER"; public static final String ENTITY_CLIENT = "CLIENT"; public static final String ENTITY_DATATABLE = "DATATABLE"; diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 9bfe1f0606e..22dde89b00a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -243,6 +243,7 @@ import static org.apache.fineract.commands.domain.CommandWrapperConstants.ENTITY_WORKINGCAPITALLOAN; import static org.apache.fineract.commands.domain.CommandWrapperConstants.ENTITY_WORKINGCAPITALLOANCHARGE; import static org.apache.fineract.commands.domain.CommandWrapperConstants.ENTITY_WORKINGCAPITALLOANPRODUCT; +import static org.apache.fineract.commands.domain.CommandWrapperConstants.ENTITY_WORKINGCAPITALLOANTRANSACTION; import static org.apache.fineract.useradministration.service.AppUserConstants.PASSWORD; import static org.apache.fineract.useradministration.service.AppUserConstants.REPEAT_PASSWORD; @@ -4068,4 +4069,13 @@ public CommandWrapperBuilder undoAccountTransfer(final Long transferId) { this.href = "/accounttransfers"; return this; } + + public CommandWrapperBuilder undoWorkingCapitalLoanTransaction(Long resolvedLoanId, Long resolvedTransactionId) { + this.actionName = ACTION_UNDO; + this.entityName = ENTITY_WORKINGCAPITALLOANTRANSACTION; + this.entityId = resolvedTransactionId; + this.loanId = resolvedLoanId; + this.href = "/working-capital-loans/" + loanId + "/transactions/" + entityId + "?command=undo"; + return this; + } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalAmortizationScheduleStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalAmortizationScheduleStepDef.java index af1eeaf0ea5..f49801770b5 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalAmortizationScheduleStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalAmortizationScheduleStepDef.java @@ -84,7 +84,12 @@ public void verifyRetrievedSummaryFields(final DataTable dataTable) { @Then("The retrieved amortization schedule has payments with the following details:") public void verifyRetrievedPaymentDetails(final DataTable dataTable) { - verifyPaymentDetails(dataTable); + verifyPaymentDetails(dataTable, null); + } + + @Then("The retrieved amortization schedule has payments with the following details in first {string} lines:") + public void verifyRetrievedPaymentDetailsFirstNLines(final String firstNLines, final DataTable dataTable) { + verifyPaymentDetails(dataTable, Integer.valueOf(firstNLines)); } private void verifySummaryFields(final DataTable dataTable) { @@ -105,11 +110,13 @@ private void verifySummaryFields(final DataTable dataTable) { assertions.assertAll(); } - private void verifyPaymentDetails(final DataTable dataTable) { + private void verifyPaymentDetails(final DataTable dataTable, final Integer firstNLines) { final ProjectedAmortizationScheduleData response = TestContext.INSTANCE.get(WC_AMORT_SCHEDULE_KEY); assertThat(response).as("Amortization schedule response").isNotNull(); - final List actualPayments = response.getPayments(); + final List actualPayments = firstNLines != null + ? response.getPayments().subList(0, firstNLines) + : response.getPayments(); assertThat(actualPayments).as("payments list").isNotNull(); final List> expectedRows = dataTable.asMaps(); diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java index 2fd38565937..5163b3fb0e4 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java @@ -53,6 +53,8 @@ import org.apache.fineract.client.feign.util.CallFailedRuntimeException; import org.apache.fineract.client.models.CommandProcessingResult; import org.apache.fineract.client.models.DeleteWorkingCapitalLoansLoanIdResponse; +import org.apache.fineract.client.models.ExecuteWorkingCapitalLoanTransactionCommandRequest; +import org.apache.fineract.client.models.ExecuteWorkingCapitalLoanTransactionCommandResponse; import org.apache.fineract.client.models.GetBalance; import org.apache.fineract.client.models.GetCodeValuesDataResponse; import org.apache.fineract.client.models.GetDisbursementDetail; @@ -3010,6 +3012,29 @@ public void verifyReversedWorkingCapitalLoanTransactionJournalEntries(final Stri verifyTransactionsJournalEntries(transactionType, transactionDate, true, null, table); } + @When("Customer undo {string}th working capital transaction made on {string}") + public void undoNthTransaction(String nthItemStr, String transactionDate) throws IOException { + final GetWorkingCapitalLoanTransactionsResponse getWorkingCapitalLoansLoanIdResponse = retrieveLoanTransactions(getCreatedLoanId()); + final List actualTransactions = getWorkingCapitalLoansLoanIdResponse.getContent(); + + int nthItem = Integer.parseInt(nthItemStr) - 1; + + GetWorkingCapitalLoanTransactionIdResponse transactionIdResponse = actualTransactions.stream() + .filter(t -> transactionDate.equals(FORMATTER.format(t.getTransactionDate()))).toList().get(nthItem); + + String reversalExternalId = Utils.randomStringGenerator("wcl-reversal-ext-id", 8); + ExecuteWorkingCapitalLoanTransactionCommandRequest request = new ExecuteWorkingCapitalLoanTransactionCommandRequest() + .reversalExternalId(reversalExternalId); + + ExecuteWorkingCapitalLoanTransactionCommandResponse undo = ok( + () -> fineractClient.workingCapitalLoanTransactions().executeWorkingCapitalLoanTransactionCommandByLoanIdTransactionId( + getCreatedLoanId(), transactionIdResponse.getId(), "undo", request)); + Assertions.assertNotNull(undo); + + // testContext().set(TestContextKey.LOAN_TRANSACTION_UNDO_RESPONSE, transactionUndoResponse); + + } + private void verifyTransactionsJournalEntries(final String transactionType, final String transactionDate, final boolean reversed, final Integer expectedCount, final DataTable table) { final Long loanId = getCreatedLoanId(); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachEvaluation.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachEvaluation.feature index e0e04fd1ea2..027baf13505 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachEvaluation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalBreachEvaluation.feature @@ -16,13 +16,18 @@ Feature: Working Capital Breach Evaluation When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount And Admin runs inline COB job for Working Capital Loan by loanId When Admin sets the business date to "15 January 2026" - And Admin makes Internal Payment "500.0" on "2026-01-15" + And Customer makes repayment on "15 January 2026" with 500.0 transaction amount on Working Capital loan When Admin sets the business date to "01 February 2026" And Admin runs inline COB job for Working Capital Loan by loanId Then Working Capital loan breach schedule has the following data: | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | | 1 | 2026-01-01 | 2026-01-31 | 31 | 500.00 | 0.00 | null | false | | 2 | 2026-02-01 | 2026-02-28 | 28 | 500.00 | 500.00 | null | null | + When Customer undo "1"th working capital transaction made on "15 January 2026" + Then Working Capital loan breach schedule has the following data: + | periodNumber | fromDate | toDate | numberOfDays | minPaymentAmount | outstandingAmount | nearBreach | breach | + | 1 | 2026-01-01 | 2026-01-31 | 31 | 500.00 | 500.00 | null | true | + | 2 | 2026-02-01 | 2026-02-28 | 28 | 500.00 | 500.00 | null | null | @TestRailId:C76609 Scenario: Verify that partial payment less than minPayment results in breach true after period end diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanUndoTransaction.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanUndoTransaction.feature new file mode 100644 index 00000000000..399f00fb0e3 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanUndoTransaction.feature @@ -0,0 +1,110 @@ +@WorkingCapital +@WorkingCapitalLoanUndoTransactionFeature +Feature: Working Capital Loan Undo Transaction + + @TestRailId:C76617 + Scenario: Verify working capital loan repayment undo + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_ACCOUNTING_CASH_BASED | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | proposedPrincipal | approvedPrincipal | totalPaymentVolume | periodPaymentRate | discountApproved | + | WCLP_ACCOUNTING_CASH_BASED | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPaymentVolume | periodPaymentRate | discount | + | WCLP_ACCOUNTING_CASH_BASED | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + + And Admin retrieves the projected amortization schedule + And The retrieved amortization schedule has payments with the following details in first "11" lines: + | paymentNo | date | expectedPaymentAmount | expectedBalance | expectedAmortizationAmount | actualPaymentAmount | actualAmortizationAmount | expectedDiscountFeeBalance | actualBalance | actualDiscountFeeBalance | + | 0 | 2026-01-01 | -9000.00 | 9000.00 | | | | 0.0 | 9000.00 | 0.00 | + | 1 | 2026-01-02 | 50.00 | 8950.00 | 0.0 | | | 0.0 | | | + | 2 | 2026-01-03 | 50.00 | 8900.00 | 0.0 | | | 0.0 | | | + | 3 | 2026-01-04 | 50.00 | 8850.00 | 0.0 | | | 0.0 | | | + | 4 | 2026-01-05 | 50.00 | 8800.00 | 0.0 | | | 0.0 | | | + | 5 | 2026-01-06 | 50.00 | 8750.00 | 0.0 | | | 0.0 | | | + | 6 | 2026-01-07 | 50.00 | 8700.00 | 0.0 | | | 0.0 | | | + | 7 | 2026-01-08 | 50.00 | 8650.00 | 0.0 | | | 0.0 | | | + | 8 | 2026-01-09 | 50.00 | 8600.00 | 0.0 | | | 0.0 | | | + | 9 | 2026-01-10 | 50.00 | 8550.00 | 0.0 | | | 0.0 | | | + | 10 | 2026-01-11 | 50.00 | 8500.00 | 0.0 | | | 0.0 | | | + + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + + And Customer makes repayment on "10 January 2026" with 270.0 transaction amount on Working Capital loan + + Then Working Capital Loan Transactions tab has a "REPAYMENT" transaction with date "10 January 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | LIABILITY | 145023 | Suspense/Clearing account | 270.0 | | + | ASSET | 112601 | Loans Receivable | | 270.0 | + + And Admin retrieves the projected amortization schedule + And The retrieved amortization schedule has payments with the following details in first "11" lines: + | paymentNo | date | expectedPaymentAmount | expectedBalance | expectedAmortizationAmount | actualPaymentAmount | actualAmortizationAmount | expectedDiscountFeeBalance | actualBalance | actualDiscountFeeBalance | + | 0 | 2026-01-01 | -9000.00 | 9000.00 | | | | 0.0 | 9000.00 | 0.00 | + | 1 | 2026-01-02 | 50.00 | 8950.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 2 | 2026-01-03 | 50.00 | 8900.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 3 | 2026-01-04 | 50.00 | 8850.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 4 | 2026-01-05 | 50.00 | 8800.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 5 | 2026-01-06 | 50.00 | 8750.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 6 | 2026-01-07 | 50.00 | 8700.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 7 | 2026-01-08 | 50.00 | 8650.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 8 | 2026-01-09 | 50.00 | 8600.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 9 | 2026-01-10 | 50.00 | 8550.00 | 0.0 | 270.0 | 0.0 | 0.0 | 8730.00 | 0.00 | + | 10 | 2026-01-11 | 50.00 | 8500.00 | 0.0 | | | 0.0 | | | + + Then Working Capital loan balance payload contains the following fields: + | field | value | + | principalOutstanding | 8730.00 | + | overpaymentAmount | 0.00 | + | totalPaidPrincipal | 270.00 | + + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + + When Customer undo "1"th working capital transaction made on "10 January 2026" + + Then Working Capital Loan Transactions tab has a reversed "REPAYMENT" transaction with date "10 January 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | LIABILITY | 145023 | Suspense/Clearing account | 270.0 | | + | ASSET | 112601 | Loans Receivable | | 270.0 | + | LIABILITY | 145023 | Suspense/Clearing account | | 270.0 | + | ASSET | 112601 | Loans Receivable | 270.0 | | + + And Admin retrieves the projected amortization schedule + And The retrieved amortization schedule has payments with the following details in first "11" lines: + | paymentNo | date | expectedPaymentAmount | expectedBalance | expectedAmortizationAmount | actualPaymentAmount | actualAmortizationAmount | expectedDiscountFeeBalance | actualBalance | actualDiscountFeeBalance | + | 0 | 2026-01-01 | -9000.00 | 9000.00 | | | | 0.0 | 9000.00 | 0.00 | + | 1 | 2026-01-02 | 50.00 | 8950.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 2 | 2026-01-03 | 50.00 | 8900.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 3 | 2026-01-04 | 50.00 | 8850.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 4 | 2026-01-05 | 50.00 | 8800.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 5 | 2026-01-06 | 50.00 | 8750.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 6 | 2026-01-07 | 50.00 | 8700.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 7 | 2026-01-08 | 50.00 | 8650.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 8 | 2026-01-09 | 50.00 | 8600.00 | 0.0 | 0.0 | 0.0 | 0.0 | 9000.00 | 0.00 | + | 9 | 2026-01-10 | 50.00 | 8550.00 | 0.0 | | | 0.0 | | | + | 10 | 2026-01-11 | 50.00 | 8500.00 | 0.0 | | | 0.0 | | | + + Then Working Capital loan balance payload contains the following fields: + | field | value | + | principalOutstanding | 9000.00 | + | overpaymentAmount | 0.00 | + | totalPaidPrincipal | 0.00 | + + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java index d57da4ff55d..1eab15d0a59 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java @@ -87,6 +87,12 @@ private WorkingCapitalLoanConstants() { public static final String WRITE_OFF_REASONS = "WriteOffReasons"; public static final String CHARGE_OFF_REASONS = "ChargeOffReasons"; + // transaction undo parameters + public static final String reversalExternalIdParamName = "reversalExternalId"; + + // Transaction Commands + public static final String UNDO_COMMAND = "undo"; + // Period payment rate change parameters public static final String periodPaymentRateParamName = "periodPaymentRate"; public static final String previousPeriodPaymentRateParamName = "previousRate"; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java index 5fd94e16a69..83f499c5576 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java @@ -32,6 +32,7 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import lombok.RequiredArgsConstructor; @@ -45,9 +46,11 @@ import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDisbursementDetails; import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; -import org.apache.fineract.portfolio.workingcapitalloan.service.InternalWorkingCapitalLoanPaymentService; import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanAmortizationScheduleWriteService; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanBreachScheduleService; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanDelinquencyClassificationService; import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanDelinquencyRangeScheduleService; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @@ -64,7 +67,10 @@ public class InternalWorkingCapitalLoanApiResource implements InitializingBean { private final WorkingCapitalLoanAmortizationScheduleWriteService writeService; private final WorkingCapitalLoanRepository loanRepository; private final WorkingCapitalLoanDelinquencyRangeScheduleService rangeScheduleService; - private final InternalWorkingCapitalLoanPaymentService paymentService; + private final WorkingCapitalLoanWritePlatformService writePlatformService; + private final WorkingCapitalLoanBreachScheduleService breachScheduleService; + private final WorkingCapitalLoanDelinquencyRangeScheduleService delinquencyRangeScheduleService; + private final WorkingCapitalLoanDelinquencyClassificationService delinquencyClassificationService; @Override @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") @@ -165,9 +171,17 @@ public Response generateNextDelinquencyPeriod(@PathParam("loanId") @Parameter(de generated during loan approval/disbursement from the loan and product data.""") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "Working Capital Loan not found") }) + @Deprecated(forRemoval = true) public void payment(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, final InternalWorkingCapitalLoanPaymentRequest request) { - paymentService.makePayment(loanId, request.getAmount(), request.getTransactionDate()); + LocalDate transactionDate = request.getTransactionDate(); + BigDecimal amount = request.getAmount(); + breachScheduleService.applyRepayment(loanId, transactionDate, amount); + delinquencyRangeScheduleService.applyRepayment(loanId, transactionDate, amount); + if (delinquencyClassificationService.instantDelinquencyClassificationIsEnabled()) { + WorkingCapitalLoan workingCapitalLoan = loanRepository.findById(loanId).orElseThrow(); + delinquencyClassificationService.classifyDelinquency(workingCapitalLoan, transactionDate); + } } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResource.java index 35d66d91904..234e2591fab 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResource.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResource.java @@ -165,11 +165,7 @@ public WorkingCapitalLoanCommandTemplateData retrieveWorkingCapitalLoanTemplate( private WorkingCapitalLoanCommandTemplateData handleLoanTransactionTemplate(final Long loanId, final String loanExternalIdStr, final String templateType) { - final Long resolvedLoanId = loanId != null ? loanId - : loanReadPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalIdStr)); - if (resolvedLoanId == null) { - throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalIdStr)); - } + final Long resolvedLoanId = resolveLoanId(loanId, loanExternalIdStr); final WorkingCapitalLoanCommandTemplateData loanTransactionTemplateData = transactionReadPlatformService .retrieveLoanTransactionTemplate(resolvedLoanId, templateType); @@ -212,11 +208,8 @@ public CommandProcessingResult executeLoanTransactionByExternalId( private CommandProcessingResult executeTransaction(final Long loanId, final String loanExternalIdStr, final String commandParam, final String apiRequestBodyAsJson) { - final Long resolvedLoanId = loanId != null ? loanId - : loanReadPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalIdStr)); - if (resolvedLoanId == null) { - throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalIdStr)); - } + final Long resolvedLoanId = resolveLoanId(loanId, loanExternalIdStr); + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); final CommandWrapper commandRequest; if (CommandParameterUtil.is(commandParam, "repayment")) { @@ -234,4 +227,93 @@ private CommandProcessingResult executeTransaction(final Long loanId, final Stri } return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); } + + private Long resolveLoanId(Long loanId, String loanExternalId) { + final Long resolvedLoanId = loanId != null ? loanId + : loanReadPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalId)); + if (resolvedLoanId == null) { + throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalId)); + } + return resolvedLoanId; + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(operationId = "executeWorkingCapitalLoanTransactionCommandByLoanIdTransactionId", summary = "Execute Working Capital Loan transaction command by loan id and transaction id", description = "Supported command query parameter: undo") + @Path("{loanId}/transactions/{transactionId}") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.ExecuteWorkingCapitalLoanTransactionCommandRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.ExecuteWorkingCapitalLoanTransactionCommandResponse.class))) }) + public CommandProcessingResult executeWorkingCapitalLoanTransactionCommandByLoanIdTransactionId( + @PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, + @PathParam("transactionId") @Parameter(description = "transactionId", required = true) final Long transactionId, + @QueryParam("command") @Parameter(description = "command", required = true) final String command, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + return executeWorkingCapitalLoanTransactionCommand(loanId, null, transactionId, null, command, apiRequestBodyAsJson); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(operationId = "executeWorkingCapitalLoanTransactionCommandByLoanIdTransactionExternalId", summary = "Execute Working Capital Loan transaction command by loan id and transaction external id", description = "Supported command query parameter: undo") + @Path("{loanId}/transactions/external-id/{transactionExternalId}") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.ExecuteWorkingCapitalLoanTransactionCommandRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.ExecuteWorkingCapitalLoanTransactionCommandResponse.class))) }) + public CommandProcessingResult executeWorkingCapitalLoanTransactionCommandByLoanIdTransactionExternalId( + @PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId, + @PathParam("transactionExternalId") @Parameter(description = "transactionExternalId", required = true) final String transactionExternalId, + @QueryParam("command") @Parameter(description = "command", required = true) final String command, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + return executeWorkingCapitalLoanTransactionCommand(loanId, null, null, transactionExternalId, command, apiRequestBodyAsJson); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(operationId = "executeWorkingCapitalLoanTransactionCommandByLoanExternalIdTransactionId", summary = "Execute Working Capital Loan transaction command by loan external id and transaction id", description = "Supported command query parameter: undo") + @Path("external-id/{loanExternalId}/transactions/{transactionId}") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.ExecuteWorkingCapitalLoanTransactionCommandRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.ExecuteWorkingCapitalLoanTransactionCommandResponse.class))) }) + public CommandProcessingResult executeWorkingCapitalLoanTransactionCommandByLoanExternalIdTransactionId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId, + @PathParam("transactionId") @Parameter(description = "transactionId", required = true) final Long transactionId, + @QueryParam("command") @Parameter(description = "command", required = true) final String command, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + return executeWorkingCapitalLoanTransactionCommand(null, loanExternalId, transactionId, null, command, apiRequestBodyAsJson); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(operationId = "executeWorkingCapitalLoanTransactionCommandByLoanExternalIdTransactionExternalId", summary = "Execute Working Capital Loan transaction command by loan external id and transaction external id", description = "Supported command query parameter: undo") + @Path("external-id/{loanExternalId}/transactions/external-id/{transactionExternalId}") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.ExecuteWorkingCapitalLoanTransactionCommandRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.ExecuteWorkingCapitalLoanTransactionCommandResponse.class))) }) + public CommandProcessingResult executeWorkingCapitalLoanTransactionCommandByLoanExternalIdTransactionExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId, + @PathParam("transactionExternalId") @Parameter(description = "transactionExternalId", required = true) final String transactionExternalId, + @QueryParam("command") @Parameter(description = "command", required = true) final String command, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + return executeWorkingCapitalLoanTransactionCommand(null, loanExternalId, null, transactionExternalId, command, + apiRequestBodyAsJson); + } + + private CommandProcessingResult executeWorkingCapitalLoanTransactionCommand(Long loanId, String loanExternalId, Long transactionId, + String transactionExternalId, String command, String apiRequestBodyAsJson) { + final Long resolvedLoanId = resolveLoanId(loanId, loanExternalId); + final Long resolvedTransactionId = resolveLoanId(transactionId, transactionExternalId); + final String commandParam = command == null ? null : command.trim().toLowerCase(); + final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); + final CommandWrapper commandRequest; + if (CommandParameterUtil.is(commandParam, WorkingCapitalLoanConstants.UNDO_COMMAND)) { + commandRequest = builder.undoWorkingCapitalLoanTransaction(resolvedLoanId, resolvedTransactionId).build(); + } else { + throw new UnrecognizedQueryParamException("command", commandParam); + } + return this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResourceSwagger.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResourceSwagger.java index 8dff5a81d7e..b6317d884f0 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResourceSwagger.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResourceSwagger.java @@ -186,4 +186,32 @@ private PostWorkingCapitalLoanTransactionsResponse() {} @Schema(example = "repayment-ext-001") public String resourceExternalId; } + + @Schema(description = "Request for working capital loan transaction command execution") + public static final class ExecuteWorkingCapitalLoanTransactionCommandRequest { + + private ExecuteWorkingCapitalLoanTransactionCommandRequest() {} + + @Schema(example = "loan-ext-001") + public String reversalExternalId; + } + + @Schema(description = "Response for working capital loan transaction command execution") + public static final class ExecuteWorkingCapitalLoanTransactionCommandResponse { + + private ExecuteWorkingCapitalLoanTransactionCommandResponse() {} + + @Schema(example = "1") + public Long officeId; + @Schema(example = "2") + public Long clientId; + @Schema(example = "3") + public Long loanId; + @Schema(example = "loan-ext-001") + public String loanExternalId; + @Schema(example = "4") + public Long resourceId; + @Schema(example = "repayment-ext-001") + public String resourceExternalId; + } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java index b687867d842..9185f6b52e1 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.Accessors; @@ -273,6 +274,20 @@ public void applyPayment(final LocalDate paymentDate, final BigDecimal amount) { rebuildPayments(); } + public void undoPayment(final LocalDate paymentDate, final BigDecimal amount) { + Objects.requireNonNull(paymentDate, "paymentDate"); + Objects.requireNonNull(amount, "amount"); + final int firstPeriodDayOffset = hasDisbursementDatePayment() || paymentDate.equals(expectedDisbursementDate) ? 0 : 1; + final LocalDate allocationDate = calculateAllocationDate(paymentDate, firstPeriodDayOffset); + Optional first = actualPayments.stream() + .filter(p -> p.date.equals(allocationDate) && p.amount.getAmount().compareTo(amount) == 0).findFirst(); + if (first.isEmpty()) { + throw new IllegalStateException("payment not found: date=" + paymentDate + " with amount=" + amount); + } + actualPayments.remove(first.get()); + rebuildPayments(); + } + private void updateCalculatedTillDate(final LocalDate actionDate) { if (calculatedTillDate == null || actionDate.isAfter(calculatedTillDate)) { this.calculatedTillDate = actionDate; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanEvent.java index 5df88c14dcc..3ca17af6f42 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanEvent.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanEvent.java @@ -27,5 +27,6 @@ public enum WorkingCapitalLoanEvent { LOAN_DISBURSAL_UNDO, // LOAN_REPAID_IN_FULL, // LOAN_OVERPAID, // + LOAN_REOPENED, // LOAN_CREDIT_BALANCE_REFUND_IN_FULL // } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanLifecycleStateMachine.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanLifecycleStateMachine.java index 6f8322edec4..687968a5d54 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanLifecycleStateMachine.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanLifecycleStateMachine.java @@ -53,6 +53,7 @@ private LoanStatus getNextStatus(final WorkingCapitalLoanEvent event, final Work case LOAN_DISBURSAL_UNDO -> from.isActive() ? LoanStatus.APPROVED : null; case LOAN_REPAID_IN_FULL -> from.isActive() ? LoanStatus.CLOSED_OBLIGATIONS_MET : null; case LOAN_OVERPAID -> (from.isActive() || from.isOverpaid()) ? LoanStatus.OVERPAID : null; + case LOAN_REOPENED -> (from.isOverpaid() || from.isClosedObligationsMet()) ? LoanStatus.ACTIVE : null; case LOAN_CREDIT_BALANCE_REFUND_IN_FULL -> from.isOverpaid() ? LoanStatus.CLOSED_OBLIGATIONS_MET : null; }; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/UndoWorkingCapitalLoanTransactionCommandHandler.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/UndoWorkingCapitalLoanTransactionCommandHandler.java new file mode 100644 index 00000000000..b559145a2a2 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/UndoWorkingCapitalLoanTransactionCommandHandler.java @@ -0,0 +1,42 @@ +/** + * 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.workingcapitalloan.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.domain.CommandWrapperConstants; +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.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@CommandType(entity = CommandWrapperConstants.ENTITY_WORKINGCAPITALLOANTRANSACTION, action = CommandWrapperConstants.ACTION_UNDO) +public class UndoWorkingCapitalLoanTransactionCommandHandler implements NewCommandSourceHandler { + + private final WorkingCapitalLoanWritePlatformService writePlatformService; + + @Override + public CommandProcessingResult processCommand(JsonCommand command) { + return writePlatformService.undoTransaction(command.getLoanId(), command.entityId(), command); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java index f6c72d08631..006c72e9d21 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyRangeScheduleRepository.java @@ -40,6 +40,8 @@ public interface WorkingCapitalLoanDelinquencyRangeScheduleRepository List findByLoanIdAndToDateIsBeforeAndMinPaymentCriteriaMet(Long loanId, LocalDate toDateBefore, Boolean minPaymentCriteriaMet); + List findByLoanIdAndToDateIsBefore(Long loanId, LocalDate toDateBefore); + Optional findByLoanIdAndFromDateLessThanEqualAndToDateGreaterThanEqual(Long loanId, LocalDate date, LocalDate date2); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java index d60d51c7c5d..1fbe20dbcc3 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java @@ -34,6 +34,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; +import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.domain.ExternalId; @@ -80,6 +81,9 @@ public class WorkingCapitalLoanDataValidator { WorkingCapitalLoanConstants.paymentDetailsParamName, WorkingCapitalLoanConstants.externalIdParameterName, WorkingCapitalLoanConstants.discountExternalIdParameterName, WorkingCapitalLoanConstants.classificationIdParamName)); + private static final Set UNDO_TRANSACTION_SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("locale", "dateFormat", + WorkingCapitalLoanConstants.reversalExternalIdParamName, WorkingCapitalLoanConstants.noteParamName)); + private static final Set PAYMENT_DETAILS_SUPPORTED_PARAMETERS = new HashSet<>( Arrays.asList(WorkingCapitalLoanConstants.paymentTypeIdParamName, WorkingCapitalLoanConstants.accountNumberParamName, WorkingCapitalLoanConstants.checkNumberParamName, WorkingCapitalLoanConstants.routingCodeParamName, @@ -811,4 +815,28 @@ private boolean isDiscountOverrideDisallowed(final WorkingCapitalLoan loan) { return loan.getLoanProduct() == null || loan.getLoanProduct().getConfigurableAttributes() == null || !loan.getLoanProduct().getConfigurableAttributes().isDiscountDefaultOverridable(); } + + public void validateUndoTransaction(JsonCommand command, WorkingCapitalLoan loan, WorkingCapitalLoanTransaction transaction) { + final String json = command.getJsonCommand(); + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UNDO_TRANSACTION_SUPPORTED_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(WorkingCapitalLoanConstants.RESOURCE_NAME); + + if (transaction.isReversed()) { + baseDataValidator.reset().parameter("transaction").failWithCode("transaction.already.undone", transaction.getId()); + } + + baseDataValidator.reset().parameter("transactionType").value(transaction.getTypeOf().getCode()).isNotOneOfTheseValues( + LoanTransactionType.DISBURSEMENT.getCode(), LoanTransactionType.DISCOUNT_FEE, LoanTransactionType.DISCOUNT_FEE_AMORTIZATION, + LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentService.java deleted file mode 100644 index a385accb48f..00000000000 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentService.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * 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.workingcapitalloan.service; - -import java.math.BigDecimal; -import java.time.LocalDate; - -public interface InternalWorkingCapitalLoanPaymentService { - - void makePayment(Long loanId, BigDecimal amount, LocalDate transactionDate); -} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentServiceImpl.java deleted file mode 100644 index 33f7678516e..00000000000 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/InternalWorkingCapitalLoanPaymentServiceImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * 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.workingcapitalloan.service; - -import static org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants.ENABLE_INSTANT_DELINQUENCY_CALCULATION; - -import java.math.BigDecimal; -import java.time.LocalDate; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper; -import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; -import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class InternalWorkingCapitalLoanPaymentServiceImpl implements InternalWorkingCapitalLoanPaymentService { - - private final WorkingCapitalLoanRepository loanRepository; - private final WorkingCapitalLoanDelinquencyRangeScheduleService delinquencyRangeScheduleService; - private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; - private final WorkingCapitalLoanDelinquencyClassificationService delinquencyClassificationService; - private final WorkingCapitalLoanBreachScheduleService breachScheduleService; - - @Override - public void makePayment(Long loanId, BigDecimal amount, LocalDate transactionDate) { - delinquencyRangeScheduleService.applyRepayment(loanId, transactionDate, amount); - breachScheduleService.applyRepayment(loanId, transactionDate, amount); - if (globalConfigurationRepository.findOneByNameWithNotFoundDetection(ENABLE_INSTANT_DELINQUENCY_CALCULATION).isEnabled()) { - WorkingCapitalLoan workingCapitalLoan = loanRepository.findById(loanId).orElseThrow(); - if (workingCapitalLoan.getLoanProductRelatedDetails() != null - && workingCapitalLoan.getLoanProductRelatedDetails().getDelinquencyBucket() != null) { - delinquencyClassificationService.classifyDelinquency(workingCapitalLoan, transactionDate, - workingCapitalLoan.getLoanProductRelatedDetails().getDelinquencyBucket()); - } - } - } -} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java index 65db465d94c..220bac79512 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java @@ -37,6 +37,8 @@ public interface WorkingCapitalLoanAmortizationScheduleWriteService { BigDecimal getWorkingCapitalLoanDiscountAmount(WorkingCapitalLoan loan); + void applyRepaymentUndo(WorkingCapitalLoan loan, LocalDate transactionDate, BigDecimal repaymentAmount); + void regenerateAmortizationScheduleOnRateChange(WorkingCapitalLoan loan, BigDecimal newRate); /** diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java index 3c0c9493d0c..cca01fd74df 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java @@ -183,6 +183,23 @@ public void applyRepayment(final WorkingCapitalLoan loan, final LocalDate transa scheduleRepositoryWrapper.writeModel(loan, model); } + @Override + public void applyRepaymentUndo(final WorkingCapitalLoan loan, final LocalDate transactionDate, final BigDecimal repaymentAmount) { + Validate.notNull(loan, "loan must not be null"); + Validate.notNull(transactionDate, "transactionDate must not be null"); + Validate.notNull(repaymentAmount, "repaymentAmount must not be null"); + + final MathContext mc = MoneyHelper.getMathContext(); + final ProjectedAmortizationScheduleModel model = scheduleRepositoryWrapper + .readModel(loan.getId(), mc, WorkingCapitalLoanCurrencyResolver.resolveCurrency(loan)) + .orElseThrow(() -> new IllegalStateException("Projected amortization schedule is not found for loan " + loan.getId())); + + model.undoPayment(transactionDate, repaymentAmount); + model.recalculateNetAmortizationAndDeferredBalanceFrom(transactionDate); + + scheduleRepositoryWrapper.writeModel(loan, model); + } + @Override public void regenerateAmortizationScheduleOnRateChange(final WorkingCapitalLoan loan, final BigDecimal newRate) { Validate.notNull(loan, "loan must not be null"); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java index 67ebee0740a..9086bb28b52 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleService.java @@ -39,5 +39,7 @@ public interface WorkingCapitalLoanBreachScheduleService { void applyRepayment(Long loanId, LocalDate transactionDate, BigDecimal amount); + void applyRepaymentUndo(Long loanId, LocalDate transactionDate, BigDecimal amount); + void evaluateBreach(WorkingCapitalLoan loan, LocalDate businessDate); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java index b34a18c8fc1..60912d15c3e 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanBreachScheduleServiceImpl.java @@ -27,6 +27,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanBreachScheduleData; @@ -146,6 +147,28 @@ private void applyRepayment(final WorkingCapitalLoanBreachSchedule period, BigDe log.debug("Applied repayment of {} to Breach Schedule period {} for WC loan {}", payAmount, period.getPeriodNumber(), loanId); } + @Override + public void applyRepaymentUndo(Long loanId, LocalDate transactionDate, BigDecimal amount) { + Optional currentPeriod = repository + .findByLoanIdAndFromDateLessThanEqualAndToDateGreaterThanEqual(loanId, transactionDate, transactionDate); + currentPeriod.ifPresent(period -> applyRepaymentUndo(period, amount, loanId)); + } + + private void applyRepaymentUndo(final WorkingCapitalLoanBreachSchedule period, BigDecimal payAmount, Long loanId) { + BigDecimal newPaidAmount = period.getPaidAmount().subtract(payAmount); + period.setPaidAmount(newPaidAmount); + period.setOutstandingAmount(period.getMinPaymentAmount().subtract(period.getPaidAmount()).max(BigDecimal.ZERO)); + if (period.getOutstandingAmount().compareTo(BigDecimal.ZERO) > 0) { + if (period.getToDate().isBefore(ThreadLocalContextUtil.getBusinessDate())) { + period.setBreach(true); + } else { + period.setBreach(null); + } + } + repository.saveAndFlush(period); + log.debug("Applied repayment undo of {} to Breach Schedule period {} for WC loan {}", payAmount, period.getPeriodNumber(), loanId); + } + @Override public void evaluateBreach(final WorkingCapitalLoan loan, final LocalDate businessDate) { final Optional relevantPeriod = repository diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java index 4b373c4363b..9a1d61bd2ab 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java @@ -25,5 +25,9 @@ public interface WorkingCapitalLoanDelinquencyClassificationService { - void classifyDelinquency(WorkingCapitalLoan loanId, LocalDate businessDate, DelinquencyBucket delinquencyBucket); + void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate); + + void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate, DelinquencyBucket delinquencyBucket); + + boolean instantDelinquencyClassificationIsEnabled(); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java index 5b58ddacba0..e5f81730f9c 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java @@ -19,6 +19,8 @@ package org.apache.fineract.portfolio.workingcapitalloan.service; +import static org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants.ENABLE_INSTANT_DELINQUENCY_CALCULATION; + import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; @@ -26,6 +28,7 @@ import java.util.Objects; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; @@ -42,6 +45,15 @@ public class WorkingCapitalLoanDelinquencyClassificationServiceImpl implements W private final WorkingCapitalLoanDelinquencyRangeScheduleRepository delinquencyRangeScheduleRepository; private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository delinquencyRangeScheduleTagHistoryRepository; + private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; + + @Override + public void classifyDelinquency(WorkingCapitalLoan workingCapitalLoan, LocalDate businessDate) { + if (workingCapitalLoan.getLoanProductRelatedDetails() != null + && workingCapitalLoan.getLoanProductRelatedDetails().getDelinquencyBucket() != null) { + classifyDelinquency(workingCapitalLoan, businessDate, workingCapitalLoan.getLoanProductRelatedDetails().getDelinquencyBucket()); + } + } /** * Classifies the delinquency of a loan based on the delinquency bucket and the business date. @@ -81,6 +93,11 @@ public void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate, } } + @Override + public boolean instantDelinquencyClassificationIsEnabled() { + return globalConfigurationRepository.findOneByNameWithNotFoundDetection(ENABLE_INSTANT_DELINQUENCY_CALCULATION).isEnabled(); + } + /** * Finds the delinquency range for a given delinquency bucket and number of days. * diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleService.java index 600eb716968..37669be4d00 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleService.java @@ -43,4 +43,5 @@ public interface WorkingCapitalLoanDelinquencyRangeScheduleService { void rescheduleMinimumPayment(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyAction rescheduleAction); + void applyRepaymentUndo(Long loanId, LocalDate transactionDate, BigDecimal amount); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java index 9119991a19c..9baf7f0a6cc 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyRangeScheduleServiceImpl.java @@ -177,6 +177,44 @@ public void applyRepayment(Long loanId, LocalDate transactionDate, BigDecimal am } } + private BigDecimal unpayPeriod(WorkingCapitalLoanDelinquencyRangeSchedule period, BigDecimal transactionAmount) { + BigDecimal unpayAmount = period.getPaidAmount().min(transactionAmount); + period.setPaidAmount(period.getPaidAmount().subtract(unpayAmount)); + period.setOutstandingAmount(period.getExpectedAmount().subtract(period.getPaidAmount()).max(BigDecimal.ZERO)); + if (period.getOutstandingAmount().compareTo(BigDecimal.ZERO) >= 0) { + period.setMinPaymentCriteriaMet(null); + period.setDelinquentAmount(null); + period.setDelinquentDays(null); + } + loanDelinquencyRangeScheduleRepository.saveAndFlush(period); + return unpayAmount; + } + + @Override + public void applyRepaymentUndo(Long loanId, LocalDate businessDate, BigDecimal amount) { + Optional currentPeriod = loanDelinquencyRangeScheduleRepository + .findByLoanIdAndFromDateLessThanEqualAndToDateGreaterThanEqual(loanId, businessDate, businessDate); + BigDecimal transactionAmount = amount; + if (currentPeriod.isPresent()) { + WorkingCapitalLoanDelinquencyRangeSchedule period = currentPeriod.get(); + BigDecimal unpayAmount = unpayPeriod(period, transactionAmount); + transactionAmount = transactionAmount.subtract(unpayAmount); + } + + if (transactionAmount.compareTo(BigDecimal.ZERO) > 0) { + List pastPeriods = loanDelinquencyRangeScheduleRepository + .findByLoanIdAndToDateIsBefore(loanId, businessDate); + for (WorkingCapitalLoanDelinquencyRangeSchedule period : pastPeriods.reversed()) { + BigDecimal unpayAmount = unpayPeriod(period, transactionAmount); + transactionAmount = transactionAmount.subtract(unpayAmount); + if (transactionAmount.compareTo(BigDecimal.ZERO) <= 0) { + break; + } + } + } + + } + @Override public void evaluateExpiredPeriods(WorkingCapitalLoan loan, LocalDate businessDate) { List unevaluatedPeriods = loanDelinquencyRangeScheduleRepository diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java index 5619b9aaf26..6a95e9a11cb 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java @@ -44,4 +44,6 @@ public interface WorkingCapitalLoanWritePlatformService { CommandProcessingResult makeGoodwillCredit(Long loanId, JsonCommand command); CommandProcessingResult updatePeriodPaymentRate(Long loanId, JsonCommand command); + + CommandProcessingResult undoTransaction(Long loanId, Long transactionId, JsonCommand command); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java index 24fe98b58e4..9a3933c114f 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java @@ -21,6 +21,7 @@ import com.google.gson.JsonElement; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -32,6 +33,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; +import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; @@ -41,6 +43,7 @@ import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanDisbursalTransactionBusinessEvent; @@ -70,6 +73,7 @@ import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelation; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelationRepository; import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; +import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanTransactionNotFoundException; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBalanceRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanNoteRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanPeriodPaymentRateChangeRepository; @@ -98,14 +102,16 @@ public class WorkingCapitalLoanWritePlatformServiceImpl implements WorkingCapita private final PaymentDetailWritePlatformService paymentDetailService; private final WorkingCapitalLoanBalanceRepository balanceRepository; private final WorkingCapitalLoanAmortizationScheduleWriteService amortizationScheduleWriteService; - private final InternalWorkingCapitalLoanPaymentService internalWorkingCapitalLoanPaymentService; private final CodeValueRepository codeValueRepository; private final BusinessEventNotifierService businessEventNotifierService; private final WorkingCapitalLoanAccountingProcessor accountingProcessor; private final WorkingCapitalLoanTransactionRelationRepository relationRepository; private final WorkingCapitalLoanPeriodPaymentRateChangeRepository rateChangeRepository; private final WorkingCapitalLoanDiscountFeeAmortizationService discountFeeAmortizationService; - private final ProjectedAmortizationScheduleRepositoryWrapper scheduleRepositoryWrapper; + private final WorkingCapitalLoanDelinquencyRangeScheduleService delinquencyRangeScheduleService; + private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; + private final WorkingCapitalLoanDelinquencyClassificationService delinquencyClassificationService; + private final WorkingCapitalLoanBreachScheduleService breachScheduleService; @Override public CommandProcessingResult approveApplication(final Long loanId, final JsonCommand command) { @@ -634,7 +640,8 @@ private CommandProcessingResult makeRepaymentLikeTransaction(final Long loanId, amortizationScheduleWriteService.applyRepayment(loan, transactionDate, amountAppliedToOutstanding); updateBalanceOnRepayment(loan, transactionAmount); - internalWorkingCapitalLoanPaymentService.makePayment(loanId, amountAppliedToOutstanding, transactionDate); + makePaymentForDelinquency(loanId, loan, amountAppliedToOutstanding, transactionDate); + breachScheduleService.applyRepayment(loanId, transactionDate, amountAppliedToOutstanding); handleStateChanges(loan, transactionDate); triggerInlineAmortizationIfLoanClosed(loan, transactionDate); @@ -708,10 +715,17 @@ private void handleStateChanges(WorkingCapitalLoan loan, LocalDate transactionDa : BigDecimal.ZERO; if (overpaymentAmount.compareTo(BigDecimal.ZERO) > 0) { this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_OVERPAID, loan); - loan.setMaturedOnDate(transactionDate); + if (loan.getMaturedOnDate() == null) { + loan.setMaturedOnDate(transactionDate); + } } else if (principalOutstanding.compareTo(BigDecimal.ZERO) == 0) { this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_REPAID_IN_FULL, loan); - loan.setMaturedOnDate(transactionDate); + if (loan.getMaturedOnDate() == null) { + loan.setMaturedOnDate(transactionDate); + } + } else if (principalOutstanding.compareTo(BigDecimal.ZERO) > 0 && loan.getMaturedOnDate() != null) { + this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_REOPENED, loan); + loan.setMaturedOnDate(null); } } } @@ -838,6 +852,68 @@ public CommandProcessingResult updatePeriodPaymentRate(final Long loanId, final .withLoanId(loanId).with(changes).build(); } + @Override + public CommandProcessingResult undoTransaction(Long loanId, Long transactionId, JsonCommand command) { + final WorkingCapitalLoan loan = loanRepository.findById(loanId).orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); + final WorkingCapitalLoanTransaction transaction = transactionRepository.findByIdAndWcLoan_Id(transactionId, loanId) + .orElseThrow(() -> new WorkingCapitalLoanTransactionNotFoundException(loanId, transactionId)); + + validator.validateUndoTransaction(command, loan, transaction); + + Map changes = new HashMap<>(); + changes.put("reversed", true); + transaction.setReversed(true); + + ExternalId reversalExternalId = externalIdFactory + .create(command.stringValueOfParameterNamedAllowingNull(WorkingCapitalLoanConstants.reversalExternalIdParamName)); + transaction.setReversalExternalId(reversalExternalId); + changes.put("reversalExternalId", reversalExternalId); + + LocalDate reversedOnDate = ThreadLocalContextUtil.getBusinessDate(); + transaction.setReversedOnDate(reversedOnDate); + changes.put("reversedOnDate", reversedOnDate); + + if (loan.getLoanProduct().getAccountingRule().isCashBased()) { + accountingProcessor.postReversalJournalEntries(loan, transaction); + } + + amortizationScheduleWriteService.applyRepaymentUndo(loan, transaction.getTransactionDate(), + transaction.getAllocation().getPrincipalPortion()); + + updateBalanceOnUndoRepayment(loan, transaction.getTransactionAmount()); + + breachScheduleService.applyRepaymentUndo(loanId, transaction.getTransactionDate(), + transaction.getAllocation().getPrincipalPortion()); + makePaymentUndoForDelinquency(loanId, loan, transaction.getAllocation().getPrincipalPortion(), + ThreadLocalContextUtil.getBusinessDate()); + + handleStateChanges(loan, transaction.getReversedOnDate()); + changes.put("status", loan.getLoanStatus()); + + handleNote(loan, command, changes); + + return new CommandProcessingResultBuilder().withLoanId(loan.getId()).withLoanExternalId(loan.getExternalId()) + .withEntityId(transaction.getId()).withEntityExternalId(transaction.getExternalId()).with(changes).build(); + } + + public void makePaymentForDelinquency(final Long loanId, final WorkingCapitalLoan loan, final BigDecimal amount, + final LocalDate transactionDate) { + delinquencyRangeScheduleService.applyRepayment(loanId, transactionDate, amount); + if (delinquencyClassificationService.instantDelinquencyClassificationIsEnabled()) { + WorkingCapitalLoan workingCapitalLoan = loan == null ? loanRepository.findById(loanId).orElseThrow() : loan; + delinquencyClassificationService.classifyDelinquency(workingCapitalLoan, transactionDate); + } + } + + public void makePaymentUndoForDelinquency(final Long loanId, final WorkingCapitalLoan loan, final BigDecimal amount, + final LocalDate transactionDate) { + delinquencyRangeScheduleService.applyRepaymentUndo(loanId, transactionDate, amount); + if (delinquencyClassificationService.instantDelinquencyClassificationIsEnabled()) { + WorkingCapitalLoan workingCapitalLoan = loan == null ? loanRepository.findById(loanId).orElseThrow() : loan; + delinquencyClassificationService.classifyDelinquency(workingCapitalLoan, transactionDate); + } + } + @Override public CommandProcessingResult makeGoodwillCredit(Long loanId, JsonCommand command) { return makeRepaymentLikeTransaction(loanId, command, LoanTransactionType.GOODWILL_CREDIT); @@ -894,6 +970,22 @@ private void updateBalanceForDiscountChange(final WorkingCapitalLoan loan, final this.balanceRepository.saveAndFlush(balance); } + private void updateBalanceOnUndoRepayment(final WorkingCapitalLoan loan, final BigDecimal transactionAmount) { + final WorkingCapitalLoanBalance balance = this.balanceRepository.findByWcLoan_Id(loan.getId()) + .orElseGet(() -> WorkingCapitalLoanBalance.createFor(loan)); + + final BigDecimal currentTotalPaidPrincipal = MathUtil.nullToZero(balance.getPrincipalPaid()); + final BigDecimal currentOverpayment = MathUtil.nullToZero(balance.getOverpaymentAmount()); + + final BigDecimal amountSubtractFromOverpayment = currentOverpayment.min(transactionAmount); + final BigDecimal amountSubtractFromPrincipal = transactionAmount.subtract(amountSubtractFromOverpayment); + + balance.setOverpaymentAmount(currentOverpayment.subtract(amountSubtractFromOverpayment)); + balance.setPrincipalPaid(currentTotalPaidPrincipal.subtract(amountSubtractFromPrincipal)); + + this.balanceRepository.saveAndFlush(balance); + } + private void updateBalanceOnRepayment(final WorkingCapitalLoan loan, final BigDecimal repaymentAmount) { final WorkingCapitalLoanBalance balance = this.balanceRepository.findByWcLoan_Id(loan.getId()) .orElseGet(() -> WorkingCapitalLoanBalance.createFor(loan));