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
@@ -34,7 +34,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -86,6 +85,7 @@ import org.apache.fineract.portfolio.collateralmanagement.domain.CollateralManag
import org.apache.fineract.portfolio.collateralmanagement.service.LoanCollateralAssembler;
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.service.Validator;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException;
@@ -265,7 +265,7 @@ public final class LoanApplicationValidator {
final Group group = groupId != null ? this.groupRepository.findOneWithNotFoundDetection(groupId) : null;
validateClientOrGroup(client, group, productId);
validateOrThrow("loan", baseDataValidator -> {
Validator.validateOrThrow("loan", baseDataValidator -> {
final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.loanTypeParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.loanTypeParameterName).value(loanTypeStr).notNull();
@@ -798,7 +798,7 @@ public final class LoanApplicationValidator {
}
private void fixedLengthValidations(final JsonElement element) {
validateOrThrow("loan", baseDataValidator -> {
Validator.validateOrThrow("loan", baseDataValidator -> {
final String transactionProcessingStrategy = this.fromApiJsonHelper
.extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element);
final Integer numberOfRepayments = this.fromApiJsonHelper
@@ -896,7 +896,7 @@ public final class LoanApplicationValidator {
loanProduct = this.loanProductRepository.findById(productId).orElseThrow(() -> new LoanProductNotFoundException(productId));
}
validateOrThrow("loan", baseDataValidator -> {
Validator.validateOrThrow("loan", baseDataValidator -> {
final JsonElement element = this.fromApiJsonHelper.parse(json);
boolean atLeastOneParameterPassedForUpdate = false;
@@ -1487,7 +1487,7 @@ public final class LoanApplicationValidator {
}
private void validateClientOrGroup(Client client, Group group, Long productId) {
validateOrThrow("loan", baseDataValidator -> {
Validator.validateOrThrow("loan", baseDataValidator -> {
if (client == null && group == null) {
baseDataValidator.reset().parameter(LoanApiConstants.clientIdParameterName).value(client).notNull();
} else {
@@ -1546,7 +1546,7 @@ public final class LoanApplicationValidator {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, undoSupportedParameters);
validateOrThrow(LOANAPPLICATION_UNDO, baseDataValidator -> {
Validator.validateOrThrow(LOANAPPLICATION_UNDO, baseDataValidator -> {
final JsonElement element = this.fromApiJsonHelper.parse(json);
final String note = "note";
@@ -1558,7 +1558,7 @@ public final class LoanApplicationValidator {
}
public void validateMinMaxConstraintValues(final JsonElement element, final LoanProduct loanProduct) {
validateOrThrow("loan", baseDataValidator -> {
Validator.validateOrThrow("loan", baseDataValidator -> {
final BigDecimal minPrincipal = loanProduct.getMinPrincipalAmount().getAmount();
final BigDecimal maxPrincipal = loanProduct.getMaxPrincipalAmount().getAmount();
final String principalParameterName = LoanApiConstants.principalParameterName;
@@ -1668,7 +1668,7 @@ public final class LoanApplicationValidator {
}
public void validateLoanMultiDisbursementDate(final JsonElement element, LocalDate expectedDisbursementDate, BigDecimal principal) {
validateOrThrow("loan", baseDataValidator -> {
Validator.validateOrThrow("loan", baseDataValidator -> {
validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal);
});
}
@@ -1742,7 +1742,7 @@ public final class LoanApplicationValidator {
}
public void validateLoanForCollaterals(final Loan loan, final BigDecimal total) {
validateOrThrow("loan", baseDataValidator -> {
Validator.validateOrThrow("loan", baseDataValidator -> {
if (loan.getProposedPrincipal().compareTo(total) >= 0) {
String errorCode = LoanApiConstants.LOAN_COLLATERAL_TOTAL_VALUE_SHOULD_BE_SUFFICIENT;
baseDataValidator.reset().parameter(LoanApiConstants.collateralsParameterName).failWithCode(errorCode);
@@ -1996,7 +1996,7 @@ public final class LoanApplicationValidator {
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);
validateOrThrow("loanapplication", baseDataValidator -> {
Validator.validateOrThrow("loanapplication", baseDataValidator -> {
final JsonElement element = this.fromApiJsonHelper.parse(json);
final BigDecimal principal = this.fromApiJsonHelper
@@ -2174,18 +2174,6 @@ public final class LoanApplicationValidator {
&& loanUtilService.isLoanRepaymentsSyncWithMeeting(loan.group(), calendar);
}
public static void validateOrThrow(String resource, Consumer<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) {
if (client != null) {
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.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.collateralmanagement.exception.LoanCollateralAmountNotSufficientException;
import org.apache.fineract.portfolio.common.service.Validator;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalanceRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
@@ -94,9 +93,9 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
@Component
@Component("loanTransactionValidator")
@AllArgsConstructor
public final class LoanTransactionValidator {
public final class LoanTransactionValidatorImpl implements LoanTransactionValidator {
private final FromJsonHelper fromApiJsonHelper;
private final LoanApplicationValidator fromApiJsonDeserializer;
@@ -107,7 +106,6 @@ public final class LoanTransactionValidator {
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
private final CalendarInstanceRepository calendarInstanceRepository;
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
@@ -116,6 +114,7 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId) {
String json = command.json();
if (StringUtils.isBlank(json)) {
@@ -125,7 +124,7 @@ public final class LoanTransactionValidator {
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getDisbursementParameters(isAccountTransfer));
LoanApplicationValidator.validateOrThrow("loan.disbursement", baseDataValidator -> {
Validator.validateOrThrow("loan.disbursement", baseDataValidator -> {
final JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate actualDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("actualDisbursementDate", element);
baseDataValidator.reset().parameter("actualDisbursementDate").value(actualDisbursementDate).notNull();
@@ -239,7 +238,7 @@ public final class LoanTransactionValidator {
});
}
public void validateDisbursementWithPostDatedChecks(final String json, final Long loanId) {
private void validateDisbursementWithPostDatedChecks(final String json, final Long loanId) {
final JsonElement jsonElement = this.fromApiJsonHelper.parse(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.disbursement");
@@ -285,7 +284,7 @@ public final class LoanTransactionValidator {
}
}
public void validateDisbursementDateWithMeetingDate(final LocalDate actualDisbursementDate, final CalendarInstance calendarInstance,
private void validateDisbursementDateWithMeetingDate(final LocalDate actualDisbursementDate, final CalendarInstance calendarInstance,
Boolean isSkipRepaymentOnFirstMonth, Integer numberOfDays) {
if (null != calendarInstance) {
final Calendar calendar = calendarInstance.getCalendar();
@@ -299,6 +298,7 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateUndoChargeOff(final String json) {
if (!StringUtils.isBlank(json)) {
final Set<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) {
if (StringUtils.isBlank(json)) {
@@ -355,6 +356,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@Override
public void validateChargebackTransaction(final String json) {
if (StringUtils.isBlank(json)) {
@@ -385,10 +387,12 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@Override
public void validateNewRepaymentTransaction(final String json) {
validatePaymentTransaction(json);
}
@Override
public void validateTransactionWithNoAmount(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
@@ -416,6 +420,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@Override
public void validateChargeOffTransaction(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
@@ -446,6 +451,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@Override
public void validateUpdateOfLoanOfficer(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
@@ -476,6 +482,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@Override
public void validateForBulkLoanReassignment(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
@@ -504,6 +511,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@Override
public void validateMarkAsFraudLoan(final String json) {
if (StringUtils.isBlank(json)) {
return;
@@ -525,6 +533,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@Override
public void validateUpdateDisbursementDateAndAmount(final String json, LoanDisbursementDetails loanDisbursementDetails) {
if (StringUtils.isBlank(json)) {
@@ -564,6 +573,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@Override
public void validateNewRefundTransaction(final String json) {
if (StringUtils.isBlank(json)) {
@@ -596,6 +606,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@Override
public void validateLoanForeclosure(final String json) {
if (StringUtils.isBlank(json)) {
@@ -625,6 +636,7 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@Override
public void validateLoanClientIsActive(final Loan loan) {
final Client client = loan.client();
if (client != null && client.isNotActive()) {
@@ -632,6 +644,7 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateLoanGroupIsActive(final Loan loan) {
final Group group = loan.group();
if (group != null && group.isNotActive()) {
@@ -639,7 +652,7 @@ public final class LoanTransactionValidator {
}
}
public void validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(Loan loan, LocalDate transactionDate,
private void validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(Loan loan, LocalDate transactionDate,
String reversedOrCreated) {
for (LoanTransaction txn : loan.getLoanTransactions()) {
if (txn.isChargeRefund() && DateUtils.isBefore(transactionDate, txn.getTransactionDate())) {
@@ -651,7 +664,7 @@ public final class LoanTransactionValidator {
}
}
public void validateLoanDisbursementIsBeforeTransactionDate(final Loan loan, final LocalDate transactionDate) {
private void validateLoanDisbursementIsBeforeTransactionDate(final Loan loan, final LocalDate transactionDate) {
if (DateUtils.isBefore(transactionDate, loan.getDisbursementDate())) {
final String errorMessage = "The transaction date cannot be before the loan disbursement date: "
+ loan.getDisbursementDate().toString();
@@ -660,14 +673,14 @@ public final class LoanTransactionValidator {
}
}
public void validateTransactionShouldNotBeInTheFuture(final LocalDate transactionDate) {
private void validateTransactionShouldNotBeInTheFuture(final LocalDate transactionDate) {
if (DateUtils.isDateInTheFuture(transactionDate)) {
final String errorMessage = "The transaction date cannot be in the future.";
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, transactionDate);
}
}
public void validateLoanHasCurrency(final Loan loan) {
private void validateLoanHasCurrency(final Loan loan) {
MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency defaultApplicationCurrency = this.applicationCurrencyRepository.findOneByCode(currency.getCode());
if (defaultApplicationCurrency == null) {
@@ -675,7 +688,7 @@ public final class LoanTransactionValidator {
}
}
public void validateClientOfficeJoiningDateIsBeforeTransactionDate(Loan loan, LocalDate transactionDate) {
private void validateClientOfficeJoiningDateIsBeforeTransactionDate(Loan loan, LocalDate transactionDate) {
if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) {
final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate();
if (DateUtils.isBefore(transactionDate, clientOfficeJoiningDate)) {
@@ -687,6 +700,7 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateActivityNotBeforeLastTransactionDate(final Loan loan, final LocalDate activityDate, final LoanEvent event) {
if (!(loan.isInterestBearingAndInterestRecalculationEnabled() || loan.loanProduct().isHoldGuaranteeFunds())
|| !loan.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE)) {
@@ -720,6 +734,7 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays,
final boolean allowTransactionsOnNonWorkingDay) {
if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) {
@@ -728,6 +743,7 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean allowTransactionsOnHoliday,
final List<Holiday> holidays) {
if (!allowTransactionsOnHoliday && HolidayUtil.isHoliday(repaymentDate, holidays)) {
@@ -736,7 +752,7 @@ public final class LoanTransactionValidator {
}
}
public void validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(Loan loan) {
private void validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(Loan loan) {
if (loan.getLoanProduct().isMultiDisburseLoan()) {
BigDecimal totalDisbursed = loan.getDisbursedAmount();
BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
@@ -748,6 +764,7 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateLoanTransactionInterestPaymentWaiver(JsonCommand command) {
final Long loanId = command.getLoanId();
Loan loan = this.loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
@@ -770,6 +787,7 @@ public final class LoanTransactionValidator {
validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan);
}
@Override
public void validateLoanTransactionInterestPaymentWaiverAfterRecalculation(Loan loan) {
// Payment allocation calculates the new total principal repaid, and it should be validated after recalculation
if (loan.getLoanProduct().isMultiDisburseLoan()) {
@@ -784,10 +802,12 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateRefund(String json) {
validatePaymentTransaction(json);
}
@Override
public void validateRefund(final Loan loan, LoanTransactionType loanTransactionType, final LocalDate transactionDate,
ScheduleGeneratorDTO scheduleGeneratorDTO) {
checkClientOrGroupActive(loan);
@@ -803,7 +823,7 @@ public final class LoanTransactionValidator {
validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan);
}
public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransactionType loanTransactionType,
private void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransactionType loanTransactionType,
final LocalDate transactionDate) {
if (loanTransactionType.isRepaymentType() && !loanTransactionType.isChargeRefund()) {
for (LoanTransaction txn : loan.getLoanTransactions()) {
@@ -816,6 +836,7 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateRefundDateIsAfterLastRepayment(final Loan loan, final LocalDate refundTransactionDate) {
final LocalDate possibleNextRefundDate = loan.possibleNextRefundDate();
@@ -824,6 +845,7 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateActivityNotBeforeClientOrGroupTransferDate(final Loan loan, final LoanEvent event, final LocalDate activityDate) {
if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) {
final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate();
@@ -947,7 +969,8 @@ public final class LoanTransactionValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
@Override
public void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
// Validate all string payment detail fields for max length
final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element);
@@ -1016,6 +1039,7 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateIfTransactionIsChargeback(final LoanTransaction chargebackTransaction) {
if (!chargebackTransaction.isChargeback()) {
final String errorMessage = "A transaction of type chargeback was expected but not received.";
@@ -1023,6 +1047,7 @@ public final class LoanTransactionValidator {
}
}
@Override
public void validateLoanRescheduleDate(final Loan loan) {
if (DateUtils.isBefore(loan.getRescheduledOnDate(), loan.getDisbursementDate())) {
final String errorMessage = "The date on which a loan is rescheduled cannot be before the loan disbursement date: "
@@ -1038,90 +1063,19 @@ public final class LoanTransactionValidator {
}
}
public void validateCapitalizedIncome(final JsonCommand command, final Long loanId) {
final String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Type typeOfMap = new TypeToken<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) {
@Override
public void validateNote(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
if (StringUtils.isNotBlank(note)) {
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
}
}
private void validateExternalId(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
@Override
public void validateExternalId(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
final String externalId = this.fromApiJsonHelper.extractStringNamed("externalId", element);
if (StringUtils.isNotBlank(externalId)) {
baseDataValidator.reset().parameter("externalId").value(externalId).notExceedingLengthOf(100);
}
}
private Set<String> getCapitalizedIncomeParameters() {
return new HashSet<>(
Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId"));
}
}
@@ -29,11 +29,12 @@ import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class LoanJournalEntryPoster {
public class LoanJournalEntryPosterImpl implements LoanJournalEntryPoster {
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
@Override
public void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
final MonetaryCurrency currency = loan.getCurrency();
@@ -1,71 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
@Slf4j
@RequiredArgsConstructor
public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlatformService {
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualActivityProcessingService loanAccrualActivityProcessingService;
@PostConstruct
public void addListeners() {
businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class, new LoanStatusChangedListener());
businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class,
new LoanAccrualActivityPostingLoanStatusChangedListener());
}
private static 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 (loan.isOpen()) {
loan.handleMaturityDateActivate();
}
}
}
private final class LoanAccrualActivityPostingLoanStatusChangedListener
implements BusinessEventListener<LoanStatusChangedBusinessEvent> {
@Override
public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
final Loan loan = event.get();
if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
LoanStatus oldStatus = event.getOldStatus();
LoanStatus newStatus = loan.getStatus();
if ((oldStatus.isClosed() || oldStatus.isOverpaid()) && newStatus.isActive()) {
loanAccrualActivityProcessingService.processAccrualActivityForLoanReopen(loan);
}
}
}
}
}
@@ -2996,53 +2996,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.build();
}
@Transactional
@Override
public CommandProcessingResult addCapitalizedIncome(final Long loanId, final JsonCommand command) {
loanTransactionValidator.validateCapitalizedIncome(command, loanId);
final Loan loan = loanAssembler.assembleFrom(loanId);
final AppUser currentUser = getAppUserIfPresent();
final Map<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) {
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) {
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.service.LoanChargeReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -354,7 +355,10 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
loanRepository, loanDelinquencyDomainService, repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository,
delinquencyEffectivePauseHelper, configurationDomainService);
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.isMultiDisburseLoan()).thenReturn(false);
Loan loan = Mockito.spy(Loan.class);
ReflectionTestUtils.setField(loan, "loanProduct", loanProduct);
ReflectionTestUtils.setField(loan, "loanStatus", LoanStatus.ACTIVE);
LoanTransaction transaction1 = Mockito.mock(LoanTransaction.class);
LoanTransaction transaction2 = Mockito.mock(LoanTransaction.class);