From 752dd4a10eb21245a1da270719d2e50dfd4cf784 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Mon, 28 Apr 2025 18:11:12 +0200 Subject: [PATCH] FINERACT-2232: Capitalized income - Add Capitalized income to Loan --- .../service/AcmeNoteWritePlatformService.java | 5 + .../portfolio/common/service/Validator.java | 43 +++ .../service/NoteWritePlatformService.java | 2 + .../fineract/test/data/TransactionType.java | 3 +- .../data/accounttype/DefaultAccountType.java | 32 +- .../factory/LoanProductsRequestFactory.java | 2 +- .../global/GLGlobalInitializerStep.java | 5 + .../LoanProductGlobalInitializerStep.java | 56 ++-- .../features/LoanCapitalizedIncome.feature | 7 +- ...InterestPauseWritePlatformServiceImpl.java | 19 +- .../portfolio/loanaccount/domain/Loan.java | 14 +- .../loanaccount/domain/LoanTransaction.java | 8 +- .../LoanTransactionValidator.java | 95 ++++++ .../service/LoanJournalEntryPoster.java | 27 ++ .../LoanStatusChangePlatformServiceImpl.java | 0 .../service/LoanWritePlatformService.java | 2 - .../domain/LoanCapitalizedIncomeBalance.java | 0 .../AddCapitalizedIncomeCommandHandler.java | 6 +- ...oanCapitalizedIncomeBalanceRepository.java | 3 +- .../CapitalizedIncomePlatformService.java | 31 ++ ...talizedIncomeWritePlatformServiceImpl.java | 120 ++++++++ ...veLoanStatusChangePlatformServiceImpl.java | 54 ++++ .../ProgressiveLoanTransactionValidator.java | 27 ++ ...ogressiveLoanTransactionValidatorImpl.java | 283 ++++++++++++++++++ .../ProgressiveLoanAccountConfiguration.java | 62 ++++ .../LoanApplicationValidator.java | 32 +- ...java => LoanTransactionValidatorImpl.java} | 136 +++------ ...r.java => LoanJournalEntryPosterImpl.java} | 3 +- ...WritePlatformServiceJpaRepositoryImpl.java | 47 --- ...WritePlatformServiceJpaRepositoryImpl.java | 8 + ...ntDelinquencyRangeEventSerializerTest.java | 4 + 31 files changed, 902 insertions(+), 234 deletions(-) create mode 100644 fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/Validator.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java rename {fineract-provider => fineract-loan}/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java (100%) rename {fineract-loan => fineract-progressive-loan}/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalance.java (100%) rename {fineract-loan => fineract-progressive-loan}/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddCapitalizedIncomeCommandHandler.java (88%) rename {fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain => fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository}/LoanCapitalizedIncomeBalanceRepository.java (89%) create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanStatusChangePlatformServiceImpl.java create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java rename fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/{LoanTransactionValidator.java => LoanTransactionValidatorImpl.java} (91%) rename fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/{LoanJournalEntryPoster.java => LoanJournalEntryPosterImpl.java} (96%) diff --git a/custom/acme/note/service/src/main/java/com/acme/fineract/portfolio/note/service/AcmeNoteWritePlatformService.java b/custom/acme/note/service/src/main/java/com/acme/fineract/portfolio/note/service/AcmeNoteWritePlatformService.java index dbd457fb7..80849842f 100644 --- a/custom/acme/note/service/src/main/java/com/acme/fineract/portfolio/note/service/AcmeNoteWritePlatformService.java +++ b/custom/acme/note/service/src/main/java/com/acme/fineract/portfolio/note/service/AcmeNoteWritePlatformService.java @@ -42,6 +42,11 @@ public class AcmeNoteWritePlatformService implements NoteWritePlatformService, I throw new UnsupportedOperationException("createNote() is not yet implemented."); } + @Override + public void createLoanNote(Long loanId, String note) { + throw new UnsupportedOperationException("createLoanNote() is not yet implemented."); + } + @Override public CommandProcessingResult updateNote(JsonCommand command) { throw new UnsupportedOperationException("updateNote() is not yet implemented."); diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/Validator.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/Validator.java new file mode 100644 index 000000000..eb1f3237f --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/Validator.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.common.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; + +public final class Validator { + + private Validator() {} + + public static void validateOrThrow(String resource, Consumer baseDataValidator) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors).resource(resource); + + baseDataValidator.accept(dataValidatorBuilder); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformService.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformService.java index 1265d53db..7ec9177ce 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformService.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformService.java @@ -26,6 +26,8 @@ public interface NoteWritePlatformService { CommandProcessingResult createNote(JsonCommand command); + void createLoanNote(Long loanId, String note); + CommandProcessingResult updateNote(JsonCommand command); CommandProcessingResult deleteNote(JsonCommand command); diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java index 418d34684..5a0f75a21 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java @@ -33,7 +33,8 @@ public enum TransactionType { CHARGE_OFF("chargeOff"), // CHARGE_ADJUSTMENT("chargeAdjustment"), // INTEREST_PAYMENT_WAIVER("interestPaymentWaiver"), // - REPAYMENT_AT_DISBURSEMENT("repaymentAtDisbursement"); // + REPAYMENT_AT_DISBURSEMENT("repaymentAtDisbursement"), // + CAPITALIZED_INCOME("capitalizedIncome"),; public final String value; diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/accounttype/DefaultAccountType.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/accounttype/DefaultAccountType.java index b1841e5af..b609cd5b1 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/accounttype/DefaultAccountType.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/accounttype/DefaultAccountType.java @@ -21,19 +21,31 @@ package org.apache.fineract.test.data.accounttype; public enum DefaultAccountType implements AccountType { // Asset - LOANS_RECEIVABLE("Loans Receivable"), INTEREST_FEE_RECEIVABLE("Interest/Fee Receivable"), OTHER_RECEIVABLES( - "Other Receivables"), UNC_RECEIVABLE("UNC Receivable"), FUND_RECEIVABLES( - "Fund Receivables"), TRANSFER_IN_SUSPENSE_ACCOUNT("Transfer in suspense account"), ASSET_TRANSFER("Asset transfer"), + LOANS_RECEIVABLE("Loans Receivable"), // + INTEREST_FEE_RECEIVABLE("Interest/Fee Receivable"), // + OTHER_RECEIVABLES("Other Receivables"), // + UNC_RECEIVABLE("UNC Receivable"), // + FUND_RECEIVABLES("Fund Receivables"), // + TRANSFER_IN_SUSPENSE_ACCOUNT("Transfer in suspense account"), // + ASSET_TRANSFER("Asset transfer"), // // Income - DEFERRED_INTEREST_REVENUE("Deferred Interest Revenue"), RETAINED_EARNINGS_PRIOR_YEAR("Retained Earnings Prior Year"), INTEREST_INCOME( - "Interest Income"), FEE_INCOME("Fee Income"), FEE_CHARGE_OFF( - "Fee Charge Off"), RECOVERIES("Recoveries"), INTEREST_INCOME_CHARGE_OFF("Interest Income Charge Off"), + DEFERRED_INTEREST_REVENUE("Deferred Interest Revenue"), // + RETAINED_EARNINGS_PRIOR_YEAR("Retained Earnings Prior Year"), // + INTEREST_INCOME("Interest Income"), // + FEE_INCOME("Fee Income"), // + FEE_CHARGE_OFF("Fee Charge Off"), // + RECOVERIES("Recoveries"), // + INTEREST_INCOME_CHARGE_OFF("Interest Income Charge Off"), // // Liability - AA_SUSPENSE_BALANCE("AA Suspense Balance"), SUSPENSE_CLEARING_ACCOUNT("Suspense/Clearing account"), OVERPAYMENT_ACCOUNT( - "Overpayment account"), + AA_SUSPENSE_BALANCE("AA Suspense Balance"), // + SUSPENSE_CLEARING_ACCOUNT("Suspense/Clearing account"), // + OVERPAYMENT_ACCOUNT("Overpayment account"), // + DEFERRED_CAPITALIZED_INCOME("Deferred Capitalized Income"), // // Expense - CREDIT_LOSS_BAD_DEBT("Credit Loss/Bad Debt"), CREDIT_LOSS_BAD_DEBT_FRAUD("Credit Loss/Bad Debt-Fraud"), GOODWILL_EXPENSE_ACCOUNT( - "Goodwill Expense Account"), WRITTEN_OFF("Written off"); + CREDIT_LOSS_BAD_DEBT("Credit Loss/Bad Debt"), // + CREDIT_LOSS_BAD_DEBT_FRAUD("Credit Loss/Bad Debt-Fraud"), // + GOODWILL_EXPENSE_ACCOUNT("Goodwill Expense Account"), // + WRITTEN_OFF("Written off"); // private final String customName; diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java index 0449730cf..87014eb90 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/factory/LoanProductsRequestFactory.java @@ -1613,7 +1613,7 @@ public class LoanProductsRequestFactory { .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)// .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)// .capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.INTEREST) - .deferredIncomeLiabilityAccountId(accountTypeResolver.resolve(DefaultAccountType.OVERPAYMENT_ACCOUNT)) + .deferredIncomeLiabilityAccountId(accountTypeResolver.resolve(DefaultAccountType.DEFERRED_CAPITALIZED_INCOME)) .incomeFromCapitalizationAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME)) .enableIncomeCapitalization(true);// } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/GLGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/GLGlobalInitializerStep.java index 8ed010793..6c17e2e65 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/GLGlobalInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/GLGlobalInitializerStep.java @@ -59,6 +59,7 @@ public class GLGlobalInitializerStep implements FineractGlobalInitializerStep { public static final String GLA_NAME_19 = "Goodwill Expense Account"; public static final String GLA_NAME_20 = "Interest Income Charge Off"; public static final String GLA_NAME_21 = "Asset transfer"; + public static final String GLA_NAME_22 = "Deferred Capitalized Income"; public static final String GLA_GL_CODE_1 = "112601"; public static final String GLA_GL_CODE_2 = "112603"; public static final String GLA_GL_CODE_3 = "145800"; @@ -80,6 +81,7 @@ public class GLGlobalInitializerStep implements FineractGlobalInitializerStep { public static final String GLA_GL_CODE_19 = "744003"; public static final String GLA_GL_CODE_20 = "404001"; public static final String GLA_GL_CODE_21 = "146000"; + public static final String GLA_GL_CODE_22 = "145024"; private final GeneralLedgerAccountApi glaApi; @@ -149,5 +151,8 @@ public class GLGlobalInitializerStep implements FineractGlobalInitializerStep { PostGLAccountsRequest postGLAccountsRequest21 = GLAccountRequestFactory.defaultGLAccountRequest(GLA_NAME_21, GLA_GL_CODE_21, GLA_TYPE_ASSET, GLA_USAGE_DETAIL, true); glaApi.createGLAccount1(postGLAccountsRequest21).execute(); + PostGLAccountsRequest postGLAccountsRequest22 = GLAccountRequestFactory.defaultGLAccountRequest(GLA_NAME_22, GLA_GL_CODE_22, + GLA_TYPE_LIABILITY, GLA_USAGE_DETAIL, true); + glaApi.createGLAccount1(postGLAccountsRequest22).execute(); } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java index 3642a7513..151373c9a 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java @@ -1024,34 +1024,6 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION, responseLoanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundInterestRecalculation); - // LP2 with progressive loan schedule + horizontal + interest EMI + actual/actual + interest refund with - // Merchant issued and Payment refund + interest recalculation + Multidisbursement - // (LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION_MULTIDISB) - String name97 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION_MULTIDISB - .getName(); - PostLoanProductsRequest loanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundRecalculationMultiDisb = loanProductsRequestFactory - .defaultLoanProductsRequestLP2Emi()// - .isInterestRecalculationEnabled(true)// - .preClosureInterestCalculationStrategy(1)// - .rescheduleStrategyMethod(4)// - .interestRecalculationCompoundingMethod(0)// - .recalculationRestFrequencyType(2)// - .recalculationRestFrequencyInterval(1)// - .name(name97)// - .multiDisburseLoan(true)// - .disallowExpectedDisbursements(true)// - .maxTrancheCount(10)// - .outstandingLoanBalance(10000.0)// - .supportedInterestRefundTypes(Arrays.asList("MERCHANT_ISSUED_REFUND", "PAYOUT_REFUND"))// - .paymentAllocation(List.of(// - createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT")));// - Response responseLoanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundInterestRecalculationMultidisb = loanProductsApi - .createLoanProduct(loanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundRecalculationMultiDisb) - .execute(); - TestContext.INSTANCE.set( - TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION_MULTIDISB, - responseLoanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundInterestRecalculationMultidisb); - // LP2 with progressive loan schedule + horizontal + interest EMI + 360/30 + multidisbursement + downpayment + // interest recalculation // 25%, auto disabled @@ -2421,6 +2393,34 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ .createLoanProduct(loanProductsRequestLP2ProgressiveAdvPaymentCapitalizedIncome).execute(); TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_CAPITALIZED_INCOME, responseLoanProductsRequestLP2ProgressiveAdvPaymentCapitalizedIncome); + + // LP2 with progressive loan schedule + horizontal + interest EMI + actual/actual + interest refund with + // Merchant issued and Payment refund + interest recalculation + Multidisbursement + // (LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION_MULTIDISB) + String name98 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION_MULTIDISB + .getName(); + PostLoanProductsRequest loanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundRecalculationMultiDisb = loanProductsRequestFactory + .defaultLoanProductsRequestLP2Emi()// + .isInterestRecalculationEnabled(true)// + .preClosureInterestCalculationStrategy(1)// + .rescheduleStrategyMethod(4)// + .interestRecalculationCompoundingMethod(0)// + .recalculationRestFrequencyType(2)// + .recalculationRestFrequencyInterval(1)// + .name(name98)// + .multiDisburseLoan(true)// + .disallowExpectedDisbursements(true)// + .maxTrancheCount(10)// + .outstandingLoanBalance(10000.0)// + .supportedInterestRefundTypes(Arrays.asList("MERCHANT_ISSUED_REFUND", "PAYOUT_REFUND"))// + .paymentAllocation(List.of(// + createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT")));// + Response responseLoanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundInterestRecalculationMultidisb = loanProductsApi + .createLoanProduct(loanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundRecalculationMultiDisb) + .execute(); + TestContext.INSTANCE.set( + TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION_MULTIDISB, + responseLoanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundInterestRecalculationMultidisb); } public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule, diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature index 144ab7201..5ee4cc2ce 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature @@ -1,6 +1,7 @@ @CapitalizedIncomeFeature Feature: Capitalized Income + @TestRailId:C3635 Scenario: As a user I want to add capitalized income to a progressive loan after disbursement When Admin sets the business date to "1 January 2024" And Admin creates a client with random data @@ -18,6 +19,6 @@ Feature: Capitalized Income | 01 January 2024 | Down Payment | 225.0 | 225.0 | 0.0 | 0.0 | 0.0 | 675.0 | false | | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 775.0 | false | Then Loan Transactions tab has a "CAPITALIZED_INCOME" transaction with date "02 January 2024" which has the following Journal entries: - | Type | Account code | Account name | Debit | Credit | - | ASSET | 112601 | Loans Receivable | 100.0 | | - | LIABILITY | 145024 | Deferred Income | | 100.0 | + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112601 | Loans Receivable | 100.0 | | + | LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 | diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java index 2a44bcc43..18e84c3b0 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java @@ -26,20 +26,17 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.function.Consumer; import lombok.AllArgsConstructor; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; -import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.portfolio.common.service.Validator; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; @@ -156,7 +153,7 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite private void validateInterestPauseDates(Loan loan, LocalDate startDate, LocalDate endDate, String dateFormat, String locale, Long currentVariationId) { - validateOrThrow(baseDataValidator -> { + Validator.validateOrThrow("InterestPause", baseDataValidator -> { baseDataValidator.reset().parameter("startDate").value(startDate).notBlank(); baseDataValidator.reset().parameter("endDate").value(endDate).notBlank(); baseDataValidator.reset().parameter("dateFormat").value(dateFormat).notBlank(); @@ -223,16 +220,4 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite e.getMessage(), e); } } - - private void validateOrThrow(Consumer baseDataValidator) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors).resource("InterestPause"); - - baseDataValidator.accept(dataValidatorBuilder); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index b7c31293e..c45b5a2f4 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -1124,12 +1124,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public BigDecimal getDisbursedAmount() { BigDecimal principal = BigDecimal.ZERO; - for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) { - if (disbursementDetail.actualDisbursementDate() != null) { - principal = principal.add(disbursementDetail.principal()); + if (isMultiDisburmentLoan()) { + for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) { + if (disbursementDetail.actualDisbursementDate() != null) { + principal = principal.add(disbursementDetail.principal()); + } } + return principal; + } else { + return getNetDisbursalAmount(); } - return principal; } public void removeDisbursementDetail() { @@ -2066,7 +2070,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { Money outstanding = Money.zero(getCurrency()); List loanTransactions = retrieveListOfTransactionsExcludeAccruals(); for (LoanTransaction loanTransaction : loanTransactions) { - if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting()) { + if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting() || loanTransaction.isCapitalizedIncome()) { outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency())) .minus(loanTransaction.getOverPaymentPortion(getCurrency())); loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 25e4c8ba1..cbd74830b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -308,8 +308,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom holidays); + + void validateLoanTransactionInterestPaymentWaiver(JsonCommand command); + + void validateLoanTransactionInterestPaymentWaiverAfterRecalculation(Loan loan); + + void validateRefund(String json); + + void validateRefund(Loan loan, LoanTransactionType loanTransactionType, LocalDate transactionDate, + ScheduleGeneratorDTO scheduleGeneratorDTO); + + void validateRefundDateIsAfterLastRepayment(Loan loan, LocalDate refundTransactionDate); + + void validateActivityNotBeforeClientOrGroupTransferDate(Loan loan, LoanEvent event, LocalDate activityDate); + + void validatePaymentDetails(DataValidatorBuilder baseDataValidator, JsonElement element); + + void validateIfTransactionIsChargeback(LoanTransaction chargebackTransaction); + + void validateLoanRescheduleDate(Loan loan); + + void validateNote(DataValidatorBuilder baseDataValidator, JsonElement element); + + void validateExternalId(DataValidatorBuilder baseDataValidator, JsonElement element); +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java new file mode 100644 index 000000000..bcc4b0fa8 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java @@ -0,0 +1,27 @@ +/** + * 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.service; + +import java.util.List; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; + +public interface LoanJournalEntryPoster { + + void postJournalEntries(Loan loan, List existingTransactionIds, List existingReversedTransactionIds); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangePlatformServiceImpl.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java index bb5c6e692..f64f2d71a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -125,6 +125,4 @@ public interface LoanWritePlatformService { CommandProcessingResult undoChargeOff(JsonCommand command); CommandProcessingResult makeRefund(Long loanId, LoanTransactionType loanTransactionType, JsonCommand command); - - CommandProcessingResult addCapitalizedIncome(Long loanId, JsonCommand command); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalance.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalance.java similarity index 100% rename from fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalance.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalance.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddCapitalizedIncomeCommandHandler.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddCapitalizedIncomeCommandHandler.java similarity index 88% rename from fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddCapitalizedIncomeCommandHandler.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddCapitalizedIncomeCommandHandler.java index de817a418..998d74fb0 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddCapitalizedIncomeCommandHandler.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/AddCapitalizedIncomeCommandHandler.java @@ -24,7 +24,7 @@ import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.infrastructure.DataIntegrityErrorHandler; 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.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomePlatformService; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.stereotype.Service; @@ -35,7 +35,7 @@ import org.springframework.transaction.annotation.Transactional; @CommandType(entity = "LOAN", action = "CAPITALIZEDINCOME") public class AddCapitalizedIncomeCommandHandler implements NewCommandSourceHandler { - private final LoanWritePlatformService writePlatformService; + private final CapitalizedIncomePlatformService capitalizedIncomePlatformService; private final DataIntegrityErrorHandler dataIntegrityErrorHandler; @Transactional @@ -43,7 +43,7 @@ public class AddCapitalizedIncomeCommandHandler implements NewCommandSourceHandl public CommandProcessingResult processCommand(final JsonCommand command) { try { - return this.writePlatformService.addCapitalizedIncome(command.getLoanId(), command); + return this.capitalizedIncomePlatformService.addCapitalizedIncome(command.getLoanId(), command); } catch (final JpaSystemException | DataIntegrityViolationException dve) { dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.capitalized.income", "Capitalized Income"); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalanceRepository.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java similarity index 89% rename from fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalanceRepository.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java index f0d84fa65..b1ac6238c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalanceRepository.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java @@ -16,9 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.fineract.portfolio.loanaccount.domain; +package org.apache.fineract.portfolio.loanaccount.repository; import java.util.List; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java new file mode 100644 index 000000000..0c1879323 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java @@ -0,0 +1,31 @@ +/** + * 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.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.springframework.transaction.annotation.Transactional; + +public interface CapitalizedIncomePlatformService { + + @Transactional + CommandProcessingResult addCapitalizedIncome(Long loanId, JsonCommand command); + + void resetBalance(Long loanId); +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java new file mode 100644 index 000000000..af4d6d344 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java @@ -0,0 +1,120 @@ +/** + * 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.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; +import org.apache.fineract.portfolio.note.service.NoteWritePlatformService; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedIncomePlatformService { + + private final ProgressiveLoanTransactionValidator loanTransactionValidator; + private final LoanAssembler loanAssembler; + private final LoanTransactionRepository loanTransactionRepository; + private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; + private final LoanJournalEntryPoster journalEntryPoster; + private final NoteWritePlatformService noteWritePlatformService; + private final ExternalIdFactory externalIdFactory; + private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository; + + @Transactional + @Override + public CommandProcessingResult addCapitalizedIncome(final Long loanId, final JsonCommand command) { + loanTransactionValidator.validateCapitalizedIncome(command, loanId); + final Loan loan = loanAssembler.assembleFrom(loanId); + final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); + final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); + final Map changes = new LinkedHashMap<>(); + // Create payment details + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + // Extract transaction details + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, "externalId"); + + // Create capitalized income transaction + final Money capitalizedIncomeAmount = calculateCapitalizedIncomeAmount(loan, transactionAmount); + final LoanTransaction capitalizedIncomeTransaction = LoanTransaction.capitalizedIncome(loan, capitalizedIncomeAmount, paymentDetail, + transactionDate, txnExternalId); + // TODO: Transaction reverse-replay + loan repayment schedule update (follow-up story) + // Update loan with capitalized income + loan.addLoanTransaction(capitalizedIncomeTransaction); + // Create capitalized income balances + createCapitalizedIncomeBalance(capitalizedIncomeTransaction); + // Save and flush (PK is set) + loanTransactionRepository.saveAndFlush(capitalizedIncomeTransaction); + // Update loan counters and save + loan.updateLoanScheduleDependentDerivedFields(); + // Update outstanding loan balances + loan.updateLoanOutstandingBalances(); + // Create a note if provided + final String noteText = command.stringValueOfParameterNamed("note"); + if (noteText != null && !noteText.isEmpty()) { + noteWritePlatformService.createLoanNote(loanId, noteText); + } + // Post journal entries + journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder().withEntityId(loan.getId()).withEntityExternalId(loan.getExternalId()).build(); + } + + @Override + public void resetBalance(final Long loanId) { + capitalizedIncomeBalanceRepository + .delete((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("loan").get("id"), loanId)); + } + + private Money calculateCapitalizedIncomeAmount(final Loan loan, final BigDecimal transactionAmount) { + return switch (loan.getLoanProductRelatedDetail().getCapitalizedIncomeCalculationType()) { + case FLAT -> Money.of(loan.getCurrency(), transactionAmount); + }; + } + + private void createCapitalizedIncomeBalance(final LoanTransaction capitalizedIncomeTransaction) { + LoanCapitalizedIncomeBalance capitalizedIncomeBalance = new LoanCapitalizedIncomeBalance(); + capitalizedIncomeBalance.setLoan(capitalizedIncomeTransaction.getLoan()); + capitalizedIncomeBalance.setLoanTransaction(capitalizedIncomeTransaction); + capitalizedIncomeBalance.setDate(capitalizedIncomeTransaction.getTransactionDate()); + capitalizedIncomeBalance.setAmount(capitalizedIncomeTransaction.getAmount()); + capitalizedIncomeBalance.setUnrecognizedAmount(capitalizedIncomeTransaction.getAmount()); + capitalizedIncomeBalanceRepository.save(capitalizedIncomeBalance); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanStatusChangePlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanStatusChangePlatformServiceImpl.java new file mode 100644 index 000000000..de25df233 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanStatusChangePlatformServiceImpl.java @@ -0,0 +1,54 @@ +/** + * 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.service; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.event.business.BusinessEventListener; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.springframework.stereotype.Component; + +@Component("progressiveLoanStatusChangePlatformService") +@Slf4j +@RequiredArgsConstructor +public class ProgressiveLoanStatusChangePlatformServiceImpl { + + private final BusinessEventNotifierService businessEventNotifierService; + private final CapitalizedIncomePlatformService capitalizedIncomeWritePlatformService; + + @PostConstruct + public void addListeners() { + businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class, new LoanStatusChangedListener()); + } + + private final class LoanStatusChangedListener implements BusinessEventListener { + + @Override + public void onBusinessEvent(LoanStatusChangedBusinessEvent event) { + final Loan loan = event.get(); + log.debug("Loan Status change for loan {}", loan.getId()); + if (!event.getOldStatus().isSubmittedAndPendingApproval() && loan.isApproved()) { + capitalizedIncomeWritePlatformService.resetBalance(loan.getId()); + } + } + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java new file mode 100644 index 000000000..192039acd --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java @@ -0,0 +1,27 @@ +/** + * 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.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; + +public interface ProgressiveLoanTransactionValidator extends LoanTransactionValidator { + + void validateCapitalizedIncome(JsonCommand command, Long loanId); +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java new file mode 100644 index 000000000..b2f306d41 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java @@ -0,0 +1,283 @@ +/** + * 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.service; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.organisation.holiday.domain.Holiday; +import org.apache.fineract.organisation.workingdays.domain.WorkingDays; +import org.apache.fineract.portfolio.common.service.Validator; +import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance; +import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; +import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; + +@Slf4j +@RequiredArgsConstructor +public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanTransactionValidator { + + private final FromJsonHelper fromApiJsonHelper; + private final LoanTransactionValidator loanTransactionValidator; + private final LoanRepositoryWrapper loanRepositoryWrapper; + private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository; + + @Override + public void validateCapitalizedIncome(final JsonCommand command, final Long loanId) { + final String json = command.json(); + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getCapitalizedIncomeParameters()); + + Validator.validateOrThrow("loan.capitalized.income", baseDataValidator -> { + final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); + validateLoanClientIsActive(loan); + validateLoanGroupIsActive(loan); + + // Validate that loan is disbursed + if (!loan.isDisbursed()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("capitalized.income.only.after.disbursement", + "Capitalized income can be added to the loan only after Disbursement"); + } + + // Validate loan is progressive + if (!loan.isProgressiveSchedule()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.progressive.loan"); + } + + // Validate income capitalization is enabled + if (!loan.getLoanProductRelatedDetail().isEnableIncomeCapitalization()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("income.capitalization.not.enabled"); + } + + // Validate loan is active + if (!loan.getStatus().isActive()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.active"); + } + + final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element); + baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull(); + + // Validate transaction date is not before disbursement date + if (transactionDate != null && loan.getDisbursementDate() != null && transactionDate.isBefore(loan.getDisbursementDate())) { + baseDataValidator.reset().parameter("transactionDate").failWithCode("before.disbursement.date", + "Transaction date cannot be before disbursement date"); + } + + final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element); + baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount(); + + // Validate total disbursement + capitalized income <= approved amount + if (transactionAmount != null) { + final BigDecimal totalDisbursed = loan.getDisbursedAmount(); + final BigDecimal existingCapitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loanId).stream() + .map(LoanCapitalizedIncomeBalance::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + final BigDecimal approvedAmount = loan.getApprovedPrincipal(); + final BigDecimal newTotal = totalDisbursed.add(existingCapitalizedIncomeBalance).add(transactionAmount); + + if (newTotal.compareTo(approvedAmount) > 0) { + baseDataValidator.reset().parameter("transactionAmount").failWithCode("exceeds.approved.amount", + "Sum of disbursed amount and capitalized income cannot exceed approved amount"); + } + } + + validatePaymentDetails(baseDataValidator, element); + validateNote(baseDataValidator, element); + validateExternalId(baseDataValidator, element); + }); + } + + // Delegates + @Override + public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId) { + loanTransactionValidator.validateDisbursement(command, isAccountTransfer, loanId); + } + + @Override + public void validateUndoChargeOff(String json) { + loanTransactionValidator.validateUndoChargeOff(json); + } + + @Override + public void validateTransaction(String json) { + loanTransactionValidator.validateTransaction(json); + } + + @Override + public void validateChargebackTransaction(String json) { + loanTransactionValidator.validateChargebackTransaction(json); + } + + @Override + public void validateNewRepaymentTransaction(String json) { + loanTransactionValidator.validateNewRepaymentTransaction(json); + } + + @Override + public void validateTransactionWithNoAmount(String json) { + loanTransactionValidator.validateTransactionWithNoAmount(json); + } + + @Override + public void validateChargeOffTransaction(String json) { + loanTransactionValidator.validateChargeOffTransaction(json); + } + + @Override + public void validateUpdateOfLoanOfficer(String json) { + loanTransactionValidator.validateUpdateOfLoanOfficer(json); + } + + @Override + public void validateForBulkLoanReassignment(String json) { + loanTransactionValidator.validateForBulkLoanReassignment(json); + } + + @Override + public void validateMarkAsFraudLoan(String json) { + loanTransactionValidator.validateMarkAsFraudLoan(json); + } + + @Override + public void validateUpdateDisbursementDateAndAmount(String json, LoanDisbursementDetails loanDisbursementDetails) { + loanTransactionValidator.validateUpdateDisbursementDateAndAmount(json, loanDisbursementDetails); + } + + @Override + public void validateNewRefundTransaction(String json) { + loanTransactionValidator.validateNewRefundTransaction(json); + + } + + @Override + public void validateLoanForeclosure(String json) { + loanTransactionValidator.validateLoanForeclosure(json); + } + + @Override + public void validateLoanClientIsActive(Loan loan) { + loanTransactionValidator.validateLoanClientIsActive(loan); + } + + @Override + public void validateLoanGroupIsActive(Loan loan) { + loanTransactionValidator.validateLoanGroupIsActive(loan); + } + + @Override + public void validateActivityNotBeforeLastTransactionDate(Loan loan, LocalDate activityDate, LoanEvent event) { + loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, activityDate, event); + } + + @Override + public void validateRepaymentDateIsOnNonWorkingDay(LocalDate repaymentDate, WorkingDays workingDays, + boolean allowTransactionsOnNonWorkingDay) { + loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(repaymentDate, workingDays, allowTransactionsOnNonWorkingDay); + } + + @Override + public void validateRepaymentDateIsOnHoliday(LocalDate repaymentDate, boolean allowTransactionsOnHoliday, List holidays) { + loanTransactionValidator.validateRepaymentDateIsOnHoliday(repaymentDate, allowTransactionsOnHoliday, holidays); + } + + @Override + public void validateLoanTransactionInterestPaymentWaiver(JsonCommand command) { + loanTransactionValidator.validateLoanTransactionInterestPaymentWaiver(command); + } + + @Override + public void validateLoanTransactionInterestPaymentWaiverAfterRecalculation(Loan loan) { + loanTransactionValidator.validateLoanTransactionInterestPaymentWaiverAfterRecalculation(loan); + } + + @Override + public void validateRefund(String json) { + loanTransactionValidator.validateRefund(json); + } + + @Override + public void validateRefund(Loan loan, LoanTransactionType loanTransactionType, LocalDate transactionDate, + ScheduleGeneratorDTO scheduleGeneratorDTO) { + loanTransactionValidator.validateRefund(loan, loanTransactionType, transactionDate, scheduleGeneratorDTO); + } + + @Override + public void validateRefundDateIsAfterLastRepayment(Loan loan, LocalDate refundTransactionDate) { + loanTransactionValidator.validateRefundDateIsAfterLastRepayment(loan, refundTransactionDate); + } + + @Override + public void validateActivityNotBeforeClientOrGroupTransferDate(Loan loan, LoanEvent event, LocalDate activityDate) { + loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, event, activityDate); + } + + @Override + public void validatePaymentDetails(DataValidatorBuilder baseDataValidator, JsonElement element) { + loanTransactionValidator.validatePaymentDetails(baseDataValidator, element); + } + + @Override + public void validateIfTransactionIsChargeback(LoanTransaction chargebackTransaction) { + loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction); + } + + @Override + public void validateLoanRescheduleDate(Loan loan) { + loanTransactionValidator.validateLoanRescheduleDate(loan); + } + + @Override + public void validateNote(DataValidatorBuilder baseDataValidator, JsonElement element) { + loanTransactionValidator.validateNote(baseDataValidator, element); + } + + @Override + public void validateExternalId(DataValidatorBuilder baseDataValidator, JsonElement element) { + loanTransactionValidator.validateExternalId(baseDataValidator, element); + } + + private Set getCapitalizedIncomeParameters() { + return new HashSet<>( + Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId")); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java new file mode 100644 index 000000000..cf0ef76b2 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java @@ -0,0 +1,62 @@ +/** + * 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.starter; + +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; +import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeWritePlatformServiceImpl; +import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster; +import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator; +import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidatorImpl; +import org.apache.fineract.portfolio.note.service.NoteWritePlatformService; +import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ProgressiveLoanAccountConfiguration { + + @Bean + @ConditionalOnMissingBean(CapitalizedIncomePlatformService.class) + public CapitalizedIncomePlatformService capitalizedIncomePlatformService(ProgressiveLoanTransactionValidator loanTransactionValidator, + LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository, + PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster, + NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory, + LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository) { + return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository, + paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory, + capitalizedIncomeBalanceRepository); + } + + @Bean + @ConditionalOnMissingBean(ProgressiveLoanTransactionValidator.class) + public ProgressiveLoanTransactionValidator progressiveLoanTransactionValidator(FromJsonHelper fromApiJsonHelper, + LoanTransactionValidator loanTransactionValidator, LoanRepositoryWrapper loanRepositoryWrapper, + LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository) { + return new ProgressiveLoanTransactionValidatorImpl(fromApiJsonHelper, loanTransactionValidator, loanRepositoryWrapper, + loanCapitalizedIncomeBalanceRepository); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java index 5ef430982..4473a137a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java @@ -34,7 +34,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.function.Consumer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -86,6 +85,7 @@ import org.apache.fineract.portfolio.collateralmanagement.domain.CollateralManag import org.apache.fineract.portfolio.collateralmanagement.service.LoanCollateralAssembler; import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; +import org.apache.fineract.portfolio.common.service.Validator; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper; import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException; @@ -265,7 +265,7 @@ public final class LoanApplicationValidator { final Group group = groupId != null ? this.groupRepository.findOneWithNotFoundDetection(groupId) : null; validateClientOrGroup(client, group, productId); - validateOrThrow("loan", baseDataValidator -> { + Validator.validateOrThrow("loan", baseDataValidator -> { final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element); baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanTypeStr).notNull(); @@ -798,7 +798,7 @@ public final class LoanApplicationValidator { } private void fixedLengthValidations(final JsonElement element) { - validateOrThrow("loan", baseDataValidator -> { + Validator.validateOrThrow("loan", baseDataValidator -> { final String transactionProcessingStrategy = this.fromApiJsonHelper .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); final Integer numberOfRepayments = this.fromApiJsonHelper @@ -896,7 +896,7 @@ public final class LoanApplicationValidator { loanProduct = this.loanProductRepository.findById(productId).orElseThrow(() -> new LoanProductNotFoundException(productId)); } - validateOrThrow("loan", baseDataValidator -> { + Validator.validateOrThrow("loan", baseDataValidator -> { final JsonElement element = this.fromApiJsonHelper.parse(json); boolean atLeastOneParameterPassedForUpdate = false; @@ -1487,7 +1487,7 @@ public final class LoanApplicationValidator { } private void validateClientOrGroup(Client client, Group group, Long productId) { - validateOrThrow("loan", baseDataValidator -> { + Validator.validateOrThrow("loan", baseDataValidator -> { if (client == null && group == null) { baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(client).notNull(); } else { @@ -1546,7 +1546,7 @@ public final class LoanApplicationValidator { }.getType(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, undoSupportedParameters); - validateOrThrow(LOANAPPLICATION_UNDO, baseDataValidator -> { + Validator.validateOrThrow(LOANAPPLICATION_UNDO, baseDataValidator -> { final JsonElement element = this.fromApiJsonHelper.parse(json); final String note = "note"; @@ -1558,7 +1558,7 @@ public final class LoanApplicationValidator { } public void validateMinMaxConstraintValues(final JsonElement element, final LoanProduct loanProduct) { - validateOrThrow("loan", baseDataValidator -> { + Validator.validateOrThrow("loan", baseDataValidator -> { final BigDecimal minPrincipal = loanProduct.getMinPrincipalAmount().getAmount(); final BigDecimal maxPrincipal = loanProduct.getMaxPrincipalAmount().getAmount(); final String principalParameterName = LoanApiConstants.principalParameterName; @@ -1668,7 +1668,7 @@ public final class LoanApplicationValidator { } public void validateLoanMultiDisbursementDate(final JsonElement element, LocalDate expectedDisbursementDate, BigDecimal principal) { - validateOrThrow("loan", baseDataValidator -> { + Validator.validateOrThrow("loan", baseDataValidator -> { validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); }); } @@ -1742,7 +1742,7 @@ public final class LoanApplicationValidator { } public void validateLoanForCollaterals(final Loan loan, final BigDecimal total) { - validateOrThrow("loan", baseDataValidator -> { + Validator.validateOrThrow("loan", baseDataValidator -> { if (loan.getProposedPrincipal().compareTo(total) >= 0) { String errorCode = LoanApiConstants.LOAN_COLLATERAL_TOTAL_VALUE_SHOULD_BE_SUFFICIENT; baseDataValidator.reset().parameter(LoanApiConstants.collateralsParameterName).failWithCode(errorCode); @@ -1996,7 +1996,7 @@ public final class LoanApplicationValidator { final Type typeOfMap = new TypeToken>() {}.getType(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); - validateOrThrow("loanapplication", baseDataValidator -> { + Validator.validateOrThrow("loanapplication", baseDataValidator -> { final JsonElement element = this.fromApiJsonHelper.parse(json); final BigDecimal principal = this.fromApiJsonHelper @@ -2174,18 +2174,6 @@ public final class LoanApplicationValidator { && loanUtilService.isLoanRepaymentsSyncWithMeeting(loan.group(), calendar); } - public static void validateOrThrow(String resource, Consumer baseDataValidator) { - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder dataValidatorBuilder = new DataValidatorBuilder(dataValidationErrors).resource(resource); - - baseDataValidator.accept(dataValidatorBuilder); - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - } - public Long resolveOfficeId(Client client, Group group) { if (client != null) { return client.getOffice().getId(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidatorImpl.java similarity index 91% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidatorImpl.java index dba73f3d7..4380ca72c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidatorImpl.java @@ -63,14 +63,13 @@ import org.apache.fineract.portfolio.calendar.exception.NotValidRecurringDateExc import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.collateralmanagement.exception.LoanCollateralAmountNotSufficientException; +import org.apache.fineract.portfolio.common.service.Validator; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance; -import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalanceRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; @@ -94,9 +93,9 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; -@Component +@Component("loanTransactionValidator") @AllArgsConstructor -public final class LoanTransactionValidator { +public final class LoanTransactionValidatorImpl implements LoanTransactionValidator { private final FromJsonHelper fromApiJsonHelper; private final LoanApplicationValidator fromApiJsonDeserializer; @@ -107,7 +106,6 @@ public final class LoanTransactionValidator { private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; private final CalendarInstanceRepository calendarInstanceRepository; private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator; - private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository; private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { @@ -116,6 +114,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId) { String json = command.json(); if (StringUtils.isBlank(json)) { @@ -125,7 +124,7 @@ public final class LoanTransactionValidator { final Type typeOfMap = new TypeToken>() {}.getType(); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getDisbursementParameters(isAccountTransfer)); - LoanApplicationValidator.validateOrThrow("loan.disbursement", baseDataValidator -> { + Validator.validateOrThrow("loan.disbursement", baseDataValidator -> { final JsonElement element = this.fromApiJsonHelper.parse(json); final LocalDate actualDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("actualDisbursementDate", element); baseDataValidator.reset().parameter("actualDisbursementDate").value(actualDisbursementDate).notNull(); @@ -239,7 +238,7 @@ public final class LoanTransactionValidator { }); } - public void validateDisbursementWithPostDatedChecks(final String json, final Long loanId) { + private void validateDisbursementWithPostDatedChecks(final String json, final Long loanId) { final JsonElement jsonElement = this.fromApiJsonHelper.parse(json); final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.disbursement"); @@ -285,7 +284,7 @@ public final class LoanTransactionValidator { } } - public void validateDisbursementDateWithMeetingDate(final LocalDate actualDisbursementDate, final CalendarInstance calendarInstance, + private void validateDisbursementDateWithMeetingDate(final LocalDate actualDisbursementDate, final CalendarInstance calendarInstance, Boolean isSkipRepaymentOnFirstMonth, Integer numberOfDays) { if (null != calendarInstance) { final Calendar calendar = calendarInstance.getCalendar(); @@ -299,6 +298,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateUndoChargeOff(final String json) { if (!StringUtils.isBlank(json)) { final Set transactionParameters = new HashSet<>(Arrays.asList(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME)); @@ -320,6 +320,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateTransaction(final String json) { if (StringUtils.isBlank(json)) { @@ -355,6 +356,7 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + @Override public void validateChargebackTransaction(final String json) { if (StringUtils.isBlank(json)) { @@ -385,10 +387,12 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + @Override public void validateNewRepaymentTransaction(final String json) { validatePaymentTransaction(json); } + @Override public void validateTransactionWithNoAmount(final String json) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); @@ -416,6 +420,7 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + @Override public void validateChargeOffTransaction(final String json) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); @@ -446,6 +451,7 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + @Override public void validateUpdateOfLoanOfficer(final String json) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); @@ -476,6 +482,7 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + @Override public void validateForBulkLoanReassignment(final String json) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); @@ -504,6 +511,7 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + @Override public void validateMarkAsFraudLoan(final String json) { if (StringUtils.isBlank(json)) { return; @@ -525,6 +533,7 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + @Override public void validateUpdateDisbursementDateAndAmount(final String json, LoanDisbursementDetails loanDisbursementDetails) { if (StringUtils.isBlank(json)) { @@ -564,6 +573,7 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + @Override public void validateNewRefundTransaction(final String json) { if (StringUtils.isBlank(json)) { @@ -596,6 +606,7 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + @Override public void validateLoanForeclosure(final String json) { if (StringUtils.isBlank(json)) { @@ -625,6 +636,7 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } + @Override public void validateLoanClientIsActive(final Loan loan) { final Client client = loan.client(); if (client != null && client.isNotActive()) { @@ -632,6 +644,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateLoanGroupIsActive(final Loan loan) { final Group group = loan.group(); if (group != null && group.isNotActive()) { @@ -639,7 +652,7 @@ public final class LoanTransactionValidator { } } - public void validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(Loan loan, LocalDate transactionDate, + private void validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(Loan loan, LocalDate transactionDate, String reversedOrCreated) { for (LoanTransaction txn : loan.getLoanTransactions()) { if (txn.isChargeRefund() && DateUtils.isBefore(transactionDate, txn.getTransactionDate())) { @@ -651,7 +664,7 @@ public final class LoanTransactionValidator { } } - public void validateLoanDisbursementIsBeforeTransactionDate(final Loan loan, final LocalDate transactionDate) { + private void validateLoanDisbursementIsBeforeTransactionDate(final Loan loan, final LocalDate transactionDate) { if (DateUtils.isBefore(transactionDate, loan.getDisbursementDate())) { final String errorMessage = "The transaction date cannot be before the loan disbursement date: " + loan.getDisbursementDate().toString(); @@ -660,14 +673,14 @@ public final class LoanTransactionValidator { } } - public void validateTransactionShouldNotBeInTheFuture(final LocalDate transactionDate) { + private void validateTransactionShouldNotBeInTheFuture(final LocalDate transactionDate) { if (DateUtils.isDateInTheFuture(transactionDate)) { final String errorMessage = "The transaction date cannot be in the future."; throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, transactionDate); } } - public void validateLoanHasCurrency(final Loan loan) { + private void validateLoanHasCurrency(final Loan loan) { MonetaryCurrency currency = loan.getCurrency(); final ApplicationCurrency defaultApplicationCurrency = this.applicationCurrencyRepository.findOneByCode(currency.getCode()); if (defaultApplicationCurrency == null) { @@ -675,7 +688,7 @@ public final class LoanTransactionValidator { } } - public void validateClientOfficeJoiningDateIsBeforeTransactionDate(Loan loan, LocalDate transactionDate) { + private void validateClientOfficeJoiningDateIsBeforeTransactionDate(Loan loan, LocalDate transactionDate) { if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) { final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate(); if (DateUtils.isBefore(transactionDate, clientOfficeJoiningDate)) { @@ -687,6 +700,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateActivityNotBeforeLastTransactionDate(final Loan loan, final LocalDate activityDate, final LoanEvent event) { if (!(loan.isInterestBearingAndInterestRecalculationEnabled() || loan.loanProduct().isHoldGuaranteeFunds()) || !loan.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE)) { @@ -720,6 +734,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) { if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) { @@ -728,6 +743,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean allowTransactionsOnHoliday, final List holidays) { if (!allowTransactionsOnHoliday && HolidayUtil.isHoliday(repaymentDate, holidays)) { @@ -736,7 +752,7 @@ public final class LoanTransactionValidator { } } - public void validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(Loan loan) { + private void validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(Loan loan) { if (loan.getLoanProduct().isMultiDisburseLoan()) { BigDecimal totalDisbursed = loan.getDisbursedAmount(); BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments(); @@ -748,6 +764,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateLoanTransactionInterestPaymentWaiver(JsonCommand command) { final Long loanId = command.getLoanId(); Loan loan = this.loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId)); @@ -770,6 +787,7 @@ public final class LoanTransactionValidator { validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan); } + @Override public void validateLoanTransactionInterestPaymentWaiverAfterRecalculation(Loan loan) { // Payment allocation calculates the new total principal repaid, and it should be validated after recalculation if (loan.getLoanProduct().isMultiDisburseLoan()) { @@ -784,10 +802,12 @@ public final class LoanTransactionValidator { } } + @Override public void validateRefund(String json) { validatePaymentTransaction(json); } + @Override public void validateRefund(final Loan loan, LoanTransactionType loanTransactionType, final LocalDate transactionDate, ScheduleGeneratorDTO scheduleGeneratorDTO) { checkClientOrGroupActive(loan); @@ -803,7 +823,7 @@ public final class LoanTransactionValidator { validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan); } - public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransactionType loanTransactionType, + private void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransactionType loanTransactionType, final LocalDate transactionDate) { if (loanTransactionType.isRepaymentType() && !loanTransactionType.isChargeRefund()) { for (LoanTransaction txn : loan.getLoanTransactions()) { @@ -816,6 +836,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateRefundDateIsAfterLastRepayment(final Loan loan, final LocalDate refundTransactionDate) { final LocalDate possibleNextRefundDate = loan.possibleNextRefundDate(); @@ -824,6 +845,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateActivityNotBeforeClientOrGroupTransferDate(final Loan loan, final LoanEvent event, final LocalDate activityDate) { if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) { final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate(); @@ -947,7 +969,8 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) { + @Override + public void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) { // Validate all string payment detail fields for max length final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element); @@ -1016,6 +1039,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateIfTransactionIsChargeback(final LoanTransaction chargebackTransaction) { if (!chargebackTransaction.isChargeback()) { final String errorMessage = "A transaction of type chargeback was expected but not received."; @@ -1023,6 +1047,7 @@ public final class LoanTransactionValidator { } } + @Override public void validateLoanRescheduleDate(final Loan loan) { if (DateUtils.isBefore(loan.getRescheduledOnDate(), loan.getDisbursementDate())) { final String errorMessage = "The date on which a loan is rescheduled cannot be before the loan disbursement date: " @@ -1038,90 +1063,19 @@ public final class LoanTransactionValidator { } } - public void validateCapitalizedIncome(final JsonCommand command, final Long loanId) { - final String json = command.json(); - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final JsonElement element = this.fromApiJsonHelper.parse(json); - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getCapitalizedIncomeParameters()); - - LoanApplicationValidator.validateOrThrow("loan.capitalized.income", baseDataValidator -> { - final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); - validateLoanClientIsActive(loan); - validateLoanGroupIsActive(loan); - - // Validate that loan is disbursed - if (!loan.isDisbursed()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("capitalized.income.only.after.disbursement", - "Capitalized income can be added to the loan only after Disbursement"); - } - - // Validate loan is progressive - if (!loan.isProgressiveSchedule()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.progressive.loan"); - } - - // Validate income capitalization is enabled - if (!loan.getLoanProductRelatedDetail().isEnableIncomeCapitalization()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("income.capitalization.not.enabled"); - } - - // Validate loan is active - if (!loan.getStatus().isActive()) { - baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.active"); - } - - final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element); - baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull(); - - // Validate transaction date is not before disbursement date - if (transactionDate != null && loan.getDisbursementDate() != null && transactionDate.isBefore(loan.getDisbursementDate())) { - baseDataValidator.reset().parameter("transactionDate").failWithCode("before.disbursement.date", - "Transaction date cannot be before disbursement date"); - } - - final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element); - baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount(); - - // Validate total disbursement + capitalized income <= approved amount - if (transactionAmount != null) { - final BigDecimal totalDisbursed = loan.getDisbursedAmount(); - final BigDecimal existingCapitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loanId).stream() - .map(LoanCapitalizedIncomeBalance::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); - final BigDecimal approvedAmount = loan.getApprovedPrincipal(); - final BigDecimal newTotal = totalDisbursed.add(existingCapitalizedIncomeBalance).add(transactionAmount); - - if (newTotal.compareTo(approvedAmount) > 0) { - baseDataValidator.reset().parameter("transactionAmount").failWithCode("exceeds.approved.amount", - "Sum of disbursed amount and capitalized income cannot exceed approved amount"); - } - } - - validatePaymentDetails(baseDataValidator, element); - validateNote(baseDataValidator, element); - validateExternalId(baseDataValidator, element); - }); - } - - private void validateNote(final DataValidatorBuilder baseDataValidator, final JsonElement element) { + @Override + public void validateNote(final DataValidatorBuilder baseDataValidator, final JsonElement element) { final String note = this.fromApiJsonHelper.extractStringNamed("note", element); if (StringUtils.isNotBlank(note)) { baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000); } } - private void validateExternalId(final DataValidatorBuilder baseDataValidator, final JsonElement element) { + @Override + public void validateExternalId(final DataValidatorBuilder baseDataValidator, final JsonElement element) { final String externalId = this.fromApiJsonHelper.extractStringNamed("externalId", element); if (StringUtils.isNotBlank(externalId)) { baseDataValidator.reset().parameter("externalId").value(externalId).notExceedingLengthOf(100); } } - - private Set getCapitalizedIncomeParameters() { - return new HashSet<>( - Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId")); - } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPosterImpl.java similarity index 96% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPosterImpl.java index 543042eaa..72c87dee3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPosterImpl.java @@ -29,11 +29,12 @@ import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor -public class LoanJournalEntryPoster { +public class LoanJournalEntryPosterImpl implements LoanJournalEntryPoster { private final JournalEntryWritePlatformService journalEntryWritePlatformService; private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; + @Override public void postJournalEntries(final Loan loan, final List existingTransactionIds, final List existingReversedTransactionIds) { final MonetaryCurrency currency = loan.getCurrency(); 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 558e9be45..88ce0dadd 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 @@ -2996,53 +2996,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .build(); } - @Transactional - @Override - public CommandProcessingResult addCapitalizedIncome(final Long loanId, final JsonCommand command) { - loanTransactionValidator.validateCapitalizedIncome(command, loanId); - - final Loan loan = loanAssembler.assembleFrom(loanId); - final AppUser currentUser = getAppUserIfPresent(); - final Map changes = new LinkedHashMap<>(); - - // Create payment details - final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - - // Validate cash payment if applicable - if (paymentDetail != null && paymentDetail.getPaymentType() != null && paymentDetail.getPaymentType().getIsCashPayment()) { - BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - this.cashierTransactionDataValidator.validateOnLoanDisbursal(currentUser, loan.getCurrencyCode(), transactionAmount); - } - - // Extract transaction details - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); - final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); - final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, "externalId"); - - // Create capitalized income transaction - final Money capitalizedIncomeAmount = Money.of(loan.getCurrency(), transactionAmount); - final LoanTransaction capitalizedIncomeTransaction = LoanTransaction.capitalizedIncome(loan, capitalizedIncomeAmount, paymentDetail, - transactionDate, txnExternalId); - - // Update loan with capitalized income - capitalizedIncomeTransaction.updateLoan(loan); - loan.addLoanTransaction(capitalizedIncomeTransaction); - loanTransactionRepository.saveAndFlush(capitalizedIncomeTransaction); - - // Update loan counters and save - loan.updateLoanScheduleDependentDerivedFields(); - - // Create note if provided - createNote(loan, command, changes); - - // Post journal entries - final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); - final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); - journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); - - return new CommandProcessingResultBuilder().withEntityId(loan.getId()).withEntityExternalId(loan.getExternalId()).build(); - } - public void handleChargebackTransaction(final Loan loan, LoanTransaction chargebackTransaction) { loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformServiceJpaRepositoryImpl.java index c3513d60c..5d50d8197 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformServiceJpaRepositoryImpl.java @@ -203,6 +203,14 @@ public class NoteWritePlatformServiceJpaRepositoryImpl implements NoteWritePlatf } + @Override + public void createLoanNote(final Long loanId, final String note) { + final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId); + final Note newNote = Note.loanNote(loan, note); + + this.noteRepository.save(newNote); + } + private String getResourceUrlFromCommand(JsonCommand command) { final String resourceUrl; diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java index a8d1be5da..07a9a070f 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java @@ -93,6 +93,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -354,7 +355,10 @@ public class LoanAccountDelinquencyRangeEventSerializerTest { loanRepository, loanDelinquencyDomainService, repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository, delinquencyEffectivePauseHelper, configurationDomainService); + LoanProduct loanProduct = Mockito.mock(LoanProduct.class); + when(loanProduct.isMultiDisburseLoan()).thenReturn(false); Loan loan = Mockito.spy(Loan.class); + ReflectionTestUtils.setField(loan, "loanProduct", loanProduct); ReflectionTestUtils.setField(loan, "loanStatus", LoanStatus.ACTIVE); LoanTransaction transaction1 = Mockito.mock(LoanTransaction.class); LoanTransaction transaction2 = Mockito.mock(LoanTransaction.class);