FINERACT-2232: Capitalized income - Add Capitalized income to Loan

This commit is contained in:
Adam Saghy
2025-04-30 10:23:55 +02:00
parent e10542838e
commit 752dd4a10e
31 changed files with 902 additions and 234 deletions
@@ -42,6 +42,11 @@ public class AcmeNoteWritePlatformService implements NoteWritePlatformService, I
throw new UnsupportedOperationException("createNote() is not yet implemented."); 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 @Override
public CommandProcessingResult updateNote(JsonCommand command) { public CommandProcessingResult updateNote(JsonCommand command) {
throw new UnsupportedOperationException("updateNote() is not yet implemented."); throw new UnsupportedOperationException("updateNote() is not yet implemented.");
@@ -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<DataValidatorBuilder> baseDataValidator) {
final List<ApiParameterError> 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);
}
}
}
@@ -26,6 +26,8 @@ public interface NoteWritePlatformService {
CommandProcessingResult createNote(JsonCommand command); CommandProcessingResult createNote(JsonCommand command);
void createLoanNote(Long loanId, String note);
CommandProcessingResult updateNote(JsonCommand command); CommandProcessingResult updateNote(JsonCommand command);
CommandProcessingResult deleteNote(JsonCommand command); CommandProcessingResult deleteNote(JsonCommand command);
@@ -33,7 +33,8 @@ public enum TransactionType {
CHARGE_OFF("chargeOff"), // CHARGE_OFF("chargeOff"), //
CHARGE_ADJUSTMENT("chargeAdjustment"), // CHARGE_ADJUSTMENT("chargeAdjustment"), //
INTEREST_PAYMENT_WAIVER("interestPaymentWaiver"), // INTEREST_PAYMENT_WAIVER("interestPaymentWaiver"), //
REPAYMENT_AT_DISBURSEMENT("repaymentAtDisbursement"); // REPAYMENT_AT_DISBURSEMENT("repaymentAtDisbursement"), //
CAPITALIZED_INCOME("capitalizedIncome"),;
public final String value; public final String value;
@@ -21,19 +21,31 @@ package org.apache.fineract.test.data.accounttype;
public enum DefaultAccountType implements AccountType { public enum DefaultAccountType implements AccountType {
// Asset // Asset
LOANS_RECEIVABLE("Loans Receivable"), INTEREST_FEE_RECEIVABLE("Interest/Fee Receivable"), OTHER_RECEIVABLES( LOANS_RECEIVABLE("Loans Receivable"), //
"Other Receivables"), UNC_RECEIVABLE("UNC Receivable"), FUND_RECEIVABLES( INTEREST_FEE_RECEIVABLE("Interest/Fee Receivable"), //
"Fund Receivables"), TRANSFER_IN_SUSPENSE_ACCOUNT("Transfer in suspense account"), ASSET_TRANSFER("Asset transfer"), 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 // Income
DEFERRED_INTEREST_REVENUE("Deferred Interest Revenue"), RETAINED_EARNINGS_PRIOR_YEAR("Retained Earnings Prior Year"), INTEREST_INCOME( DEFERRED_INTEREST_REVENUE("Deferred Interest Revenue"), //
"Interest Income"), FEE_INCOME("Fee Income"), FEE_CHARGE_OFF( RETAINED_EARNINGS_PRIOR_YEAR("Retained Earnings Prior Year"), //
"Fee Charge Off"), RECOVERIES("Recoveries"), INTEREST_INCOME_CHARGE_OFF("Interest Income Charge Off"), 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 // Liability
AA_SUSPENSE_BALANCE("AA Suspense Balance"), SUSPENSE_CLEARING_ACCOUNT("Suspense/Clearing account"), OVERPAYMENT_ACCOUNT( AA_SUSPENSE_BALANCE("AA Suspense Balance"), //
"Overpayment account"), SUSPENSE_CLEARING_ACCOUNT("Suspense/Clearing account"), //
OVERPAYMENT_ACCOUNT("Overpayment account"), //
DEFERRED_CAPITALIZED_INCOME("Deferred Capitalized Income"), //
// Expense // Expense
CREDIT_LOSS_BAD_DEBT("Credit Loss/Bad Debt"), CREDIT_LOSS_BAD_DEBT_FRAUD("Credit Loss/Bad Debt-Fraud"), GOODWILL_EXPENSE_ACCOUNT( CREDIT_LOSS_BAD_DEBT("Credit Loss/Bad Debt"), //
"Goodwill Expense Account"), WRITTEN_OFF("Written off"); CREDIT_LOSS_BAD_DEBT_FRAUD("Credit Loss/Bad Debt-Fraud"), //
GOODWILL_EXPENSE_ACCOUNT("Goodwill Expense Account"), //
WRITTEN_OFF("Written off"); //
private final String customName; private final String customName;
@@ -1613,7 +1613,7 @@ public class LoanProductsRequestFactory {
.capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)// .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)//
.capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)// .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)//
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.INTEREST) .capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.INTEREST)
.deferredIncomeLiabilityAccountId(accountTypeResolver.resolve(DefaultAccountType.OVERPAYMENT_ACCOUNT)) .deferredIncomeLiabilityAccountId(accountTypeResolver.resolve(DefaultAccountType.DEFERRED_CAPITALIZED_INCOME))
.incomeFromCapitalizationAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME)) .incomeFromCapitalizationAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME))
.enableIncomeCapitalization(true);// .enableIncomeCapitalization(true);//
} }
@@ -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_19 = "Goodwill Expense Account";
public static final String GLA_NAME_20 = "Interest Income Charge Off"; 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_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_1 = "112601";
public static final String GLA_GL_CODE_2 = "112603"; public static final String GLA_GL_CODE_2 = "112603";
public static final String GLA_GL_CODE_3 = "145800"; 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_19 = "744003";
public static final String GLA_GL_CODE_20 = "404001"; 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_21 = "146000";
public static final String GLA_GL_CODE_22 = "145024";
private final GeneralLedgerAccountApi glaApi; private final GeneralLedgerAccountApi glaApi;
@@ -149,5 +151,8 @@ public class GLGlobalInitializerStep implements FineractGlobalInitializerStep {
PostGLAccountsRequest postGLAccountsRequest21 = GLAccountRequestFactory.defaultGLAccountRequest(GLA_NAME_21, GLA_GL_CODE_21, PostGLAccountsRequest postGLAccountsRequest21 = GLAccountRequestFactory.defaultGLAccountRequest(GLA_NAME_21, GLA_GL_CODE_21,
GLA_TYPE_ASSET, GLA_USAGE_DETAIL, true); GLA_TYPE_ASSET, GLA_USAGE_DETAIL, true);
glaApi.createGLAccount1(postGLAccountsRequest21).execute(); 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();
} }
} }
@@ -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, TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION,
responseLoanProductsRequestLP2AdvancedpaymentInterestEmiActualActualInterestRefundInterestRecalculation); 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<PostLoanProductsResponse> 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 + // LP2 with progressive loan schedule + horizontal + interest EMI + 360/30 + multidisbursement + downpayment +
// interest recalculation // interest recalculation
// 25%, auto disabled // 25%, auto disabled
@@ -2421,6 +2393,34 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
.createLoanProduct(loanProductsRequestLP2ProgressiveAdvPaymentCapitalizedIncome).execute(); .createLoanProduct(loanProductsRequestLP2ProgressiveAdvPaymentCapitalizedIncome).execute();
TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_CAPITALIZED_INCOME, TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_CAPITALIZED_INCOME,
responseLoanProductsRequestLP2ProgressiveAdvPaymentCapitalizedIncome); 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<PostLoanProductsResponse> 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, public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
@@ -1,6 +1,7 @@
@CapitalizedIncomeFeature @CapitalizedIncomeFeature
Feature: Capitalized Income Feature: Capitalized Income
@TestRailId:C3635
Scenario: As a user I want to add capitalized income to a progressive loan after disbursement 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" When Admin sets the business date to "1 January 2024"
And Admin creates a client with random data 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 | | 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 | | 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: 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 | | Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | 100.0 | | | ASSET | 112601 | Loans Receivable | 100.0 | |
| LIABILITY | 145024 | Deferred Income | | 100.0 | | LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 |
@@ -26,20 +26,17 @@ import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer;
import lombok.AllArgsConstructor; 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.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; 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.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; 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.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; 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, private void validateInterestPauseDates(Loan loan, LocalDate startDate, LocalDate endDate, String dateFormat, String locale,
Long currentVariationId) { Long currentVariationId) {
validateOrThrow(baseDataValidator -> { Validator.validateOrThrow("InterestPause", baseDataValidator -> {
baseDataValidator.reset().parameter("startDate").value(startDate).notBlank(); baseDataValidator.reset().parameter("startDate").value(startDate).notBlank();
baseDataValidator.reset().parameter("endDate").value(endDate).notBlank(); baseDataValidator.reset().parameter("endDate").value(endDate).notBlank();
baseDataValidator.reset().parameter("dateFormat").value(dateFormat).notBlank(); baseDataValidator.reset().parameter("dateFormat").value(dateFormat).notBlank();
@@ -223,16 +220,4 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite
e.getMessage(), e); e.getMessage(), e);
} }
} }
private void validateOrThrow(Consumer<DataValidatorBuilder> baseDataValidator) {
final List<ApiParameterError> 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);
}
}
} }
@@ -1124,12 +1124,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public BigDecimal getDisbursedAmount() { public BigDecimal getDisbursedAmount() {
BigDecimal principal = BigDecimal.ZERO; BigDecimal principal = BigDecimal.ZERO;
for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) { if (isMultiDisburmentLoan()) {
if (disbursementDetail.actualDisbursementDate() != null) { for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) {
principal = principal.add(disbursementDetail.principal()); if (disbursementDetail.actualDisbursementDate() != null) {
principal = principal.add(disbursementDetail.principal());
}
} }
return principal;
} else {
return getNetDisbursalAmount();
} }
return principal;
} }
public void removeDisbursementDetail() { public void removeDisbursementDetail() {
@@ -2066,7 +2070,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
Money outstanding = Money.zero(getCurrency()); Money outstanding = Money.zero(getCurrency());
List<LoanTransaction> loanTransactions = retrieveListOfTransactionsExcludeAccruals(); List<LoanTransaction> loanTransactions = retrieveListOfTransactionsExcludeAccruals();
for (LoanTransaction loanTransaction : loanTransactions) { for (LoanTransaction loanTransaction : loanTransactions) {
if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting()) { if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting() || loanTransaction.isCapitalizedIncome()) {
outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency())) outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency()))
.minus(loanTransaction.getOverPaymentPortion(getCurrency())); .minus(loanTransaction.getOverPaymentPortion(getCurrency()));
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
@@ -308,8 +308,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
public static LoanTransaction capitalizedIncome(final Loan loan, final Money amount, final PaymentDetail paymentDetail, public static LoanTransaction capitalizedIncome(final Loan loan, final Money amount, final PaymentDetail paymentDetail,
final LocalDate transactionDate, final ExternalId externalId) { final LocalDate transactionDate, final ExternalId externalId) {
return new LoanTransaction(null, loan.getOffice(), LoanTransactionType.CAPITALIZED_INCOME, paymentDetail, amount.getAmount(), return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CAPITALIZED_INCOME, transactionDate, amount.getAmount(),
transactionDate, externalId); amount.getAmount(), null, null, null, null, false, paymentDetail, externalId);
} }
public LoanTransaction copyTransactionPropertiesAndMappings() { public LoanTransaction copyTransactionPropertiesAndMappings() {
@@ -632,6 +632,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
return LoanTransactionType.WAIVE_CHARGES.equals(getTypeOf()) && isNotReversed(); return LoanTransactionType.WAIVE_CHARGES.equals(getTypeOf()) && isNotReversed();
} }
public boolean isCapitalizedIncome() {
return LoanTransactionType.CAPITALIZED_INCOME.equals(getTypeOf()) && isNotReversed();
}
public boolean isWaiver() { public boolean isWaiver() {
return isInterestWaiver() || isChargesWaiver(); return isInterestWaiver() || isChargesWaiver();
} }
@@ -0,0 +1,95 @@
/**
* 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.serialization;
import com.google.gson.JsonElement;
import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
public interface LoanTransactionValidator {
void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId);
void validateUndoChargeOff(String json);
void validateTransaction(String json);
void validateChargebackTransaction(String json);
void validateNewRepaymentTransaction(String json);
void validateTransactionWithNoAmount(String json);
void validateChargeOffTransaction(String json);
void validateUpdateOfLoanOfficer(String json);
void validateForBulkLoanReassignment(String json);
void validateMarkAsFraudLoan(String json);
void validateUpdateDisbursementDateAndAmount(String json, LoanDisbursementDetails loanDisbursementDetails);
void validateNewRefundTransaction(String json);
void validateLoanForeclosure(String json);
void validateLoanClientIsActive(Loan loan);
void validateLoanGroupIsActive(Loan loan);
void validateActivityNotBeforeLastTransactionDate(Loan loan, LocalDate activityDate, LoanEvent event);
void validateRepaymentDateIsOnNonWorkingDay(LocalDate repaymentDate, WorkingDays workingDays, boolean allowTransactionsOnNonWorkingDay);
void validateRepaymentDateIsOnHoliday(LocalDate repaymentDate, boolean allowTransactionsOnHoliday, List<Holiday> 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);
}
@@ -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<Long> existingTransactionIds, List<Long> existingReversedTransactionIds);
}
@@ -125,6 +125,4 @@ public interface LoanWritePlatformService {
CommandProcessingResult undoChargeOff(JsonCommand command); CommandProcessingResult undoChargeOff(JsonCommand command);
CommandProcessingResult makeRefund(Long loanId, LoanTransactionType loanTransactionType, JsonCommand command); CommandProcessingResult makeRefund(Long loanId, LoanTransactionType loanTransactionType, JsonCommand command);
CommandProcessingResult addCapitalizedIncome(Long loanId, JsonCommand command);
} }
@@ -24,7 +24,7 @@ import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler; import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; 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.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -35,7 +35,7 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "LOAN", action = "CAPITALIZEDINCOME") @CommandType(entity = "LOAN", action = "CAPITALIZEDINCOME")
public class AddCapitalizedIncomeCommandHandler implements NewCommandSourceHandler { public class AddCapitalizedIncomeCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService; private final CapitalizedIncomePlatformService capitalizedIncomePlatformService;
private final DataIntegrityErrorHandler dataIntegrityErrorHandler; private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
@Transactional @Transactional
@@ -43,7 +43,7 @@ public class AddCapitalizedIncomeCommandHandler implements NewCommandSourceHandl
public CommandProcessingResult processCommand(final JsonCommand command) { public CommandProcessingResult processCommand(final JsonCommand command) {
try { try {
return this.writePlatformService.addCapitalizedIncome(command.getLoanId(), command); return this.capitalizedIncomePlatformService.addCapitalizedIncome(command.getLoanId(), command);
} catch (final JpaSystemException | DataIntegrityViolationException dve) { } catch (final JpaSystemException | DataIntegrityViolationException dve) {
dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.capitalized.income", dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.capitalized.income",
"Capitalized Income"); "Capitalized Income");
@@ -16,9 +16,10 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.loanaccount.domain; package org.apache.fineract.portfolio.loanaccount.repository;
import java.util.List; 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.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -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);
}
@@ -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<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
final Map<String, Object> 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);
}
}
@@ -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<LoanStatusChangedBusinessEvent> {
@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());
}
}
}
}
@@ -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);
}
@@ -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<Map<String, Object>>() {}.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<Holiday> 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<String> getCapitalizedIncomeParameters() {
return new HashSet<>(
Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId"));
}
}
@@ -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);
}
}
@@ -34,7 +34,6 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; 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.collateralmanagement.service.LoanCollateralAssembler;
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType; 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.Group;
import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper; import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException; 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; final Group group = groupId != null ? this.groupRepository.findOneWithNotFoundDetection(groupId) : null;
validateClientOrGroup(client, group, productId); validateClientOrGroup(client, group, productId);
validateOrThrow("loan", baseDataValidator -> { Validator.validateOrThrow("loan", baseDataValidator -> {
final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element); final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanTypeStr).notNull(); baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanTypeStr).notNull();
@@ -798,7 +798,7 @@ public final class LoanApplicationValidator {
} }
private void fixedLengthValidations(final JsonElement element) { private void fixedLengthValidations(final JsonElement element) {
validateOrThrow("loan", baseDataValidator -> { Validator.validateOrThrow("loan", baseDataValidator -> {
final String transactionProcessingStrategy = this.fromApiJsonHelper final String transactionProcessingStrategy = this.fromApiJsonHelper
.extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element);
final Integer numberOfRepayments = this.fromApiJsonHelper final Integer numberOfRepayments = this.fromApiJsonHelper
@@ -896,7 +896,7 @@ public final class LoanApplicationValidator {
loanProduct = this.loanProductRepository.findById(productId).orElseThrow(() -> new LoanProductNotFoundException(productId)); loanProduct = this.loanProductRepository.findById(productId).orElseThrow(() -> new LoanProductNotFoundException(productId));
} }
validateOrThrow("loan", baseDataValidator -> { Validator.validateOrThrow("loan", baseDataValidator -> {
final JsonElement element = this.fromApiJsonHelper.parse(json); final JsonElement element = this.fromApiJsonHelper.parse(json);
boolean atLeastOneParameterPassedForUpdate = false; boolean atLeastOneParameterPassedForUpdate = false;
@@ -1487,7 +1487,7 @@ public final class LoanApplicationValidator {
} }
private void validateClientOrGroup(Client client, Group group, Long productId) { private void validateClientOrGroup(Client client, Group group, Long productId) {
validateOrThrow("loan", baseDataValidator -> { Validator.validateOrThrow("loan", baseDataValidator -> {
if (client == null && group == null) { if (client == null && group == null) {
baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(client).notNull(); baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(client).notNull();
} else { } else {
@@ -1546,7 +1546,7 @@ public final class LoanApplicationValidator {
}.getType(); }.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, undoSupportedParameters); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, undoSupportedParameters);
validateOrThrow(LOANAPPLICATION_UNDO, baseDataValidator -> { Validator.validateOrThrow(LOANAPPLICATION_UNDO, baseDataValidator -> {
final JsonElement element = this.fromApiJsonHelper.parse(json); final JsonElement element = this.fromApiJsonHelper.parse(json);
final String note = "note"; final String note = "note";
@@ -1558,7 +1558,7 @@ public final class LoanApplicationValidator {
} }
public void validateMinMaxConstraintValues(final JsonElement element, final LoanProduct loanProduct) { public void validateMinMaxConstraintValues(final JsonElement element, final LoanProduct loanProduct) {
validateOrThrow("loan", baseDataValidator -> { Validator.validateOrThrow("loan", baseDataValidator -> {
final BigDecimal minPrincipal = loanProduct.getMinPrincipalAmount().getAmount(); final BigDecimal minPrincipal = loanProduct.getMinPrincipalAmount().getAmount();
final BigDecimal maxPrincipal = loanProduct.getMaxPrincipalAmount().getAmount(); final BigDecimal maxPrincipal = loanProduct.getMaxPrincipalAmount().getAmount();
final String principalParameterName = LoanApiConstants.principalParameterName; final String principalParameterName = LoanApiConstants.principalParameterName;
@@ -1668,7 +1668,7 @@ public final class LoanApplicationValidator {
} }
public void validateLoanMultiDisbursementDate(final JsonElement element, LocalDate expectedDisbursementDate, BigDecimal principal) { public void validateLoanMultiDisbursementDate(final JsonElement element, LocalDate expectedDisbursementDate, BigDecimal principal) {
validateOrThrow("loan", baseDataValidator -> { Validator.validateOrThrow("loan", baseDataValidator -> {
validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal);
}); });
} }
@@ -1742,7 +1742,7 @@ public final class LoanApplicationValidator {
} }
public void validateLoanForCollaterals(final Loan loan, final BigDecimal total) { public void validateLoanForCollaterals(final Loan loan, final BigDecimal total) {
validateOrThrow("loan", baseDataValidator -> { Validator.validateOrThrow("loan", baseDataValidator -> {
if (loan.getProposedPrincipal().compareTo(total) >= 0) { if (loan.getProposedPrincipal().compareTo(total) >= 0) {
String errorCode = LoanApiConstants.LOAN_COLLATERAL_TOTAL_VALUE_SHOULD_BE_SUFFICIENT; String errorCode = LoanApiConstants.LOAN_COLLATERAL_TOTAL_VALUE_SHOULD_BE_SUFFICIENT;
baseDataValidator.reset().parameter(LoanApiConstants.collateralsParameterName).failWithCode(errorCode); baseDataValidator.reset().parameter(LoanApiConstants.collateralsParameterName).failWithCode(errorCode);
@@ -1996,7 +1996,7 @@ public final class LoanApplicationValidator {
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters); this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);
validateOrThrow("loanapplication", baseDataValidator -> { Validator.validateOrThrow("loanapplication", baseDataValidator -> {
final JsonElement element = this.fromApiJsonHelper.parse(json); final JsonElement element = this.fromApiJsonHelper.parse(json);
final BigDecimal principal = this.fromApiJsonHelper final BigDecimal principal = this.fromApiJsonHelper
@@ -2174,18 +2174,6 @@ public final class LoanApplicationValidator {
&& loanUtilService.isLoanRepaymentsSyncWithMeeting(loan.group(), calendar); && loanUtilService.isLoanRepaymentsSyncWithMeeting(loan.group(), calendar);
} }
public static void validateOrThrow(String resource, Consumer<DataValidatorBuilder> baseDataValidator) {
final List<ApiParameterError> 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) { public Long resolveOfficeId(Client client, Group group) {
if (client != null) { if (client != null) {
return client.getOffice().getId(); return client.getOffice().getId();
@@ -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.domain.Client;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.collateralmanagement.exception.LoanCollateralAmountNotSufficientException; 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.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; 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.LoanCollateralManagement;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; 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.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component("loanTransactionValidator")
@AllArgsConstructor @AllArgsConstructor
public final class LoanTransactionValidator { public final class LoanTransactionValidatorImpl implements LoanTransactionValidator {
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
private final LoanApplicationValidator fromApiJsonDeserializer; private final LoanApplicationValidator fromApiJsonDeserializer;
@@ -107,7 +106,6 @@ public final class LoanTransactionValidator {
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
private final CalendarInstanceRepository calendarInstanceRepository; private final CalendarInstanceRepository calendarInstanceRepository;
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator; private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) { private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) { if (!dataValidationErrors.isEmpty()) {
@@ -116,6 +114,7 @@ public final class LoanTransactionValidator {
} }
} }
@Override
public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId) { public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId) {
String json = command.json(); String json = command.json();
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
@@ -125,7 +124,7 @@ public final class LoanTransactionValidator {
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getDisbursementParameters(isAccountTransfer)); 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 JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate actualDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("actualDisbursementDate", element); final LocalDate actualDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("actualDisbursementDate", element);
baseDataValidator.reset().parameter("actualDisbursementDate").value(actualDisbursementDate).notNull(); 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 JsonElement jsonElement = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.disbursement"); 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) { Boolean isSkipRepaymentOnFirstMonth, Integer numberOfDays) {
if (null != calendarInstance) { if (null != calendarInstance) {
final Calendar calendar = calendarInstance.getCalendar(); final Calendar calendar = calendarInstance.getCalendar();
@@ -299,6 +298,7 @@ public final class LoanTransactionValidator {
} }
} }
@Override
public void validateUndoChargeOff(final String json) { public void validateUndoChargeOff(final String json) {
if (!StringUtils.isBlank(json)) { if (!StringUtils.isBlank(json)) {
final Set<String> transactionParameters = new HashSet<>(Arrays.asList(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME)); final Set<String> 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) { public void validateTransaction(final String json) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
@@ -355,6 +356,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@Override
public void validateChargebackTransaction(final String json) { public void validateChargebackTransaction(final String json) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
@@ -385,10 +387,12 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@Override
public void validateNewRepaymentTransaction(final String json) { public void validateNewRepaymentTransaction(final String json) {
validatePaymentTransaction(json); validatePaymentTransaction(json);
} }
@Override
public void validateTransactionWithNoAmount(final String json) { public void validateTransactionWithNoAmount(final String json) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
@@ -416,6 +420,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@Override
public void validateChargeOffTransaction(final String json) { public void validateChargeOffTransaction(final String json) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
@@ -446,6 +451,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@Override
public void validateUpdateOfLoanOfficer(final String json) { public void validateUpdateOfLoanOfficer(final String json) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
@@ -476,6 +482,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@Override
public void validateForBulkLoanReassignment(final String json) { public void validateForBulkLoanReassignment(final String json) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
@@ -504,6 +511,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@Override
public void validateMarkAsFraudLoan(final String json) { public void validateMarkAsFraudLoan(final String json) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
return; return;
@@ -525,6 +533,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@Override
public void validateUpdateDisbursementDateAndAmount(final String json, LoanDisbursementDetails loanDisbursementDetails) { public void validateUpdateDisbursementDateAndAmount(final String json, LoanDisbursementDetails loanDisbursementDetails) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
@@ -564,6 +573,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@Override
public void validateNewRefundTransaction(final String json) { public void validateNewRefundTransaction(final String json) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
@@ -596,6 +606,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@Override
public void validateLoanForeclosure(final String json) { public void validateLoanForeclosure(final String json) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
@@ -625,6 +636,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@Override
public void validateLoanClientIsActive(final Loan loan) { public void validateLoanClientIsActive(final Loan loan) {
final Client client = loan.client(); final Client client = loan.client();
if (client != null && client.isNotActive()) { if (client != null && client.isNotActive()) {
@@ -632,6 +644,7 @@ public final class LoanTransactionValidator {
} }
} }
@Override
public void validateLoanGroupIsActive(final Loan loan) { public void validateLoanGroupIsActive(final Loan loan) {
final Group group = loan.group(); final Group group = loan.group();
if (group != null && group.isNotActive()) { 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) { String reversedOrCreated) {
for (LoanTransaction txn : loan.getLoanTransactions()) { for (LoanTransaction txn : loan.getLoanTransactions()) {
if (txn.isChargeRefund() && DateUtils.isBefore(transactionDate, txn.getTransactionDate())) { 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())) { if (DateUtils.isBefore(transactionDate, loan.getDisbursementDate())) {
final String errorMessage = "The transaction date cannot be before the loan disbursement date: " final String errorMessage = "The transaction date cannot be before the loan disbursement date: "
+ loan.getDisbursementDate().toString(); + 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)) { if (DateUtils.isDateInTheFuture(transactionDate)) {
final String errorMessage = "The transaction date cannot be in the future."; final String errorMessage = "The transaction date cannot be in the future.";
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, transactionDate); 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(); MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency defaultApplicationCurrency = this.applicationCurrencyRepository.findOneByCode(currency.getCode()); final ApplicationCurrency defaultApplicationCurrency = this.applicationCurrencyRepository.findOneByCode(currency.getCode());
if (defaultApplicationCurrency == null) { 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) { if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) {
final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate(); final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate();
if (DateUtils.isBefore(transactionDate, clientOfficeJoiningDate)) { 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) { public void validateActivityNotBeforeLastTransactionDate(final Loan loan, final LocalDate activityDate, final LoanEvent event) {
if (!(loan.isInterestBearingAndInterestRecalculationEnabled() || loan.loanProduct().isHoldGuaranteeFunds()) if (!(loan.isInterestBearingAndInterestRecalculationEnabled() || loan.loanProduct().isHoldGuaranteeFunds())
|| !loan.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE)) { || !loan.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE)) {
@@ -720,6 +734,7 @@ public final class LoanTransactionValidator {
} }
} }
@Override
public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays, public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays,
final boolean allowTransactionsOnNonWorkingDay) { final boolean allowTransactionsOnNonWorkingDay) {
if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) { if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) {
@@ -728,6 +743,7 @@ public final class LoanTransactionValidator {
} }
} }
@Override
public void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean allowTransactionsOnHoliday, public void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean allowTransactionsOnHoliday,
final List<Holiday> holidays) { final List<Holiday> holidays) {
if (!allowTransactionsOnHoliday && HolidayUtil.isHoliday(repaymentDate, 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()) { if (loan.getLoanProduct().isMultiDisburseLoan()) {
BigDecimal totalDisbursed = loan.getDisbursedAmount(); BigDecimal totalDisbursed = loan.getDisbursedAmount();
BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments(); BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
@@ -748,6 +764,7 @@ public final class LoanTransactionValidator {
} }
} }
@Override
public void validateLoanTransactionInterestPaymentWaiver(JsonCommand command) { public void validateLoanTransactionInterestPaymentWaiver(JsonCommand command) {
final Long loanId = command.getLoanId(); final Long loanId = command.getLoanId();
Loan loan = this.loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId)); Loan loan = this.loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
@@ -770,6 +787,7 @@ public final class LoanTransactionValidator {
validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan); validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan);
} }
@Override
public void validateLoanTransactionInterestPaymentWaiverAfterRecalculation(Loan loan) { public void validateLoanTransactionInterestPaymentWaiverAfterRecalculation(Loan loan) {
// Payment allocation calculates the new total principal repaid, and it should be validated after recalculation // Payment allocation calculates the new total principal repaid, and it should be validated after recalculation
if (loan.getLoanProduct().isMultiDisburseLoan()) { if (loan.getLoanProduct().isMultiDisburseLoan()) {
@@ -784,10 +802,12 @@ public final class LoanTransactionValidator {
} }
} }
@Override
public void validateRefund(String json) { public void validateRefund(String json) {
validatePaymentTransaction(json); validatePaymentTransaction(json);
} }
@Override
public void validateRefund(final Loan loan, LoanTransactionType loanTransactionType, final LocalDate transactionDate, public void validateRefund(final Loan loan, LoanTransactionType loanTransactionType, final LocalDate transactionDate,
ScheduleGeneratorDTO scheduleGeneratorDTO) { ScheduleGeneratorDTO scheduleGeneratorDTO) {
checkClientOrGroupActive(loan); checkClientOrGroupActive(loan);
@@ -803,7 +823,7 @@ public final class LoanTransactionValidator {
validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan); validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan);
} }
public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransactionType loanTransactionType, private void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransactionType loanTransactionType,
final LocalDate transactionDate) { final LocalDate transactionDate) {
if (loanTransactionType.isRepaymentType() && !loanTransactionType.isChargeRefund()) { if (loanTransactionType.isRepaymentType() && !loanTransactionType.isChargeRefund()) {
for (LoanTransaction txn : loan.getLoanTransactions()) { for (LoanTransaction txn : loan.getLoanTransactions()) {
@@ -816,6 +836,7 @@ public final class LoanTransactionValidator {
} }
} }
@Override
public void validateRefundDateIsAfterLastRepayment(final Loan loan, final LocalDate refundTransactionDate) { public void validateRefundDateIsAfterLastRepayment(final Loan loan, final LocalDate refundTransactionDate) {
final LocalDate possibleNextRefundDate = loan.possibleNextRefundDate(); 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) { public void validateActivityNotBeforeClientOrGroupTransferDate(final Loan loan, final LoanEvent event, final LocalDate activityDate) {
if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) { if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) {
final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate(); final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate();
@@ -947,7 +969,8 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors); 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 // Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element); final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element);
@@ -1016,6 +1039,7 @@ public final class LoanTransactionValidator {
} }
} }
@Override
public void validateIfTransactionIsChargeback(final LoanTransaction chargebackTransaction) { public void validateIfTransactionIsChargeback(final LoanTransaction chargebackTransaction) {
if (!chargebackTransaction.isChargeback()) { if (!chargebackTransaction.isChargeback()) {
final String errorMessage = "A transaction of type chargeback was expected but not received."; 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) { public void validateLoanRescheduleDate(final Loan loan) {
if (DateUtils.isBefore(loan.getRescheduledOnDate(), loan.getDisbursementDate())) { 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: " 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) { @Override
final String json = command.json(); public void validateNote(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.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) {
final String note = this.fromApiJsonHelper.extractStringNamed("note", element); final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
if (StringUtils.isNotBlank(note)) { if (StringUtils.isNotBlank(note)) {
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000); 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); final String externalId = this.fromApiJsonHelper.extractStringNamed("externalId", element);
if (StringUtils.isNotBlank(externalId)) { if (StringUtils.isNotBlank(externalId)) {
baseDataValidator.reset().parameter("externalId").value(externalId).notExceedingLengthOf(100); baseDataValidator.reset().parameter("externalId").value(externalId).notExceedingLengthOf(100);
} }
} }
private Set<String> getCapitalizedIncomeParameters() {
return new HashSet<>(
Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId"));
}
} }
@@ -29,11 +29,12 @@ import org.springframework.stereotype.Component;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoanJournalEntryPoster { public class LoanJournalEntryPosterImpl implements LoanJournalEntryPoster {
private final JournalEntryWritePlatformService journalEntryWritePlatformService; private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
@Override
public void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds, public void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) { final List<Long> existingReversedTransactionIds) {
final MonetaryCurrency currency = loan.getCurrency(); final MonetaryCurrency currency = loan.getCurrency();
@@ -2996,53 +2996,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.build(); .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<String, Object> 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<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> 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) { public void handleChargebackTransaction(final Loan loan, LoanTransaction chargebackTransaction) {
loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction); loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction);
@@ -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) { private String getResourceUrlFromCommand(JsonCommand command) {
final String resourceUrl; final String resourceUrl;
@@ -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.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; 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.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -354,7 +355,10 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
loanRepository, loanDelinquencyDomainService, repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository, loanRepository, loanDelinquencyDomainService, repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository,
delinquencyEffectivePauseHelper, configurationDomainService); delinquencyEffectivePauseHelper, configurationDomainService);
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.isMultiDisburseLoan()).thenReturn(false);
Loan loan = Mockito.spy(Loan.class); Loan loan = Mockito.spy(Loan.class);
ReflectionTestUtils.setField(loan, "loanProduct", loanProduct);
ReflectionTestUtils.setField(loan, "loanStatus", LoanStatus.ACTIVE); ReflectionTestUtils.setField(loan, "loanStatus", LoanStatus.ACTIVE);
LoanTransaction transaction1 = Mockito.mock(LoanTransaction.class); LoanTransaction transaction1 = Mockito.mock(LoanTransaction.class);
LoanTransaction transaction2 = Mockito.mock(LoanTransaction.class); LoanTransaction transaction2 = Mockito.mock(LoanTransaction.class);