FINERACT-2232: Capitalized income adjustment
This commit is contained in:
+1
-1
@@ -335,7 +335,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
|
||||
case CREDIT_BALANCE_REFUND -> handleCreditBalanceRefund(loanTransaction, ctx);
|
||||
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
|
||||
WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER ->
|
||||
WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER, CAPITALIZED_INCOME_ADJUSTMENT ->
|
||||
handleRepayment(loanTransaction, ctx);
|
||||
case INTEREST_REFUND -> handleInterestRefund(loanTransaction, ctx);
|
||||
case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx);
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomePlatformService;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@CommandType(entity = "LOAN", action = "CAPITALIZEDINCOMEADJUSTMENT")
|
||||
public class CapitalizedIncomeAdjustmentCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final CapitalizedIncomePlatformService capitalizedIncomePlatformService;
|
||||
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
|
||||
try {
|
||||
return this.capitalizedIncomePlatformService.capitalizedIncomeAdjustment(command.getLoanId(), command.entityId(), command);
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||
dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.capitalized.income",
|
||||
"Capitalized Income");
|
||||
return CommandProcessingResult.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -43,4 +43,7 @@ public interface LoanCapitalizedIncomeBalanceRepository
|
||||
|
||||
@Query("SELECT SUM(lcib.amountAdjustment) FROM LoanCapitalizedIncomeBalance lcib WHERE lcib.loan.id = :loanId")
|
||||
BigDecimal calculateCapitalizedIncomeAdjustment(Long loanId);
|
||||
|
||||
@Query("SELECT lcib FROM LoanCapitalizedIncomeBalance lcib, LoanTransaction lt, LoanTransactionRelation ltr WHERE lt.loan.id = lcib.loan.id AND ltr.fromTransaction.id =:transactionId AND ltr.toTransaction.id=lt.id AND lcib.loanTransaction.id = lt.id")
|
||||
LoanCapitalizedIncomeBalance findBalanceForAdjustment(Long transactionId);
|
||||
}
|
||||
|
||||
+55
-2
@@ -24,6 +24,7 @@ import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
@@ -32,10 +33,13 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.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.LoanTransactionRelation;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
|
||||
@@ -103,11 +107,60 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
||||
.build();
|
||||
}
|
||||
|
||||
private void recalculateLoanTransactions(Loan loan, LocalDate transactionDate, LoanTransaction capitalizedIncomeTransaction) {
|
||||
@Override
|
||||
public CommandProcessingResult capitalizedIncomeAdjustment(final Long loanId, final Long capitalizedIncomeTransactionId,
|
||||
final JsonCommand command) {
|
||||
loanTransactionValidator.validateCapitalizedIncomeAdjustment(command, loanId, capitalizedIncomeTransactionId);
|
||||
final Loan loan = loanAssembler.assembleFrom(loanId);
|
||||
final List<Long> existingTransactionIds = new ArrayList<>(loanTransactionRepository.findTransactionIdsByLoan(loan));
|
||||
final List<Long> existingReversedTransactionIds = new ArrayList<>(loanTransactionRepository.findReversedTransactionIdsByLoan(loan));
|
||||
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");
|
||||
|
||||
Optional<LoanTransaction> capitalizedIncome = loanTransactionRepository.findById(capitalizedIncomeTransactionId);
|
||||
LoanTransaction capitalizedIncomeAdjustment = LoanTransaction.capitalizedIncomeAdjustment(loan,
|
||||
Money.of(loan.getCurrency(), transactionAmount), paymentDetail, transactionDate, txnExternalId);
|
||||
capitalizedIncomeAdjustment.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(capitalizedIncomeAdjustment,
|
||||
capitalizedIncome.get(), LoanTransactionRelationTypeEnum.ADJUSTMENT));
|
||||
loan.addLoanTransaction(capitalizedIncomeAdjustment);
|
||||
recalculateLoanTransactions(loan, transactionDate, capitalizedIncomeAdjustment);
|
||||
LoanTransaction savedCapitalizedIncomeAdjustment = loanTransactionRepository.saveAndFlush(capitalizedIncomeAdjustment);
|
||||
|
||||
// Update outstanding loan balances
|
||||
loan.updateLoanOutstandingBalances();
|
||||
|
||||
// Create a note if provided
|
||||
final String noteText = command.stringValueOfParameterNamed("note");
|
||||
if (noteText != null && !noteText.isEmpty()) {
|
||||
noteWritePlatformService.createLoanTransactionNote(savedCapitalizedIncomeAdjustment.getId(), noteText);
|
||||
}
|
||||
// Post journal entries
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
|
||||
// Accounting uses the original balance, balance update HAS TO HAPPEN AFTER postJournalEntries
|
||||
LoanCapitalizedIncomeBalance capitalizedIncomeBalance = capitalizedIncomeBalanceRepository.findByLoanIdAndLoanTransactionId(loanId,
|
||||
capitalizedIncomeTransactionId);
|
||||
capitalizedIncomeBalance
|
||||
.setAmountAdjustment(MathUtil.nullToZero(capitalizedIncomeBalance.getAmountAdjustment()).add(transactionAmount));
|
||||
capitalizedIncomeBalance.setUnrecognizedAmount(
|
||||
MathUtil.negativeToZero(capitalizedIncomeBalance.getUnrecognizedAmount().subtract(transactionAmount)));
|
||||
capitalizedIncomeBalanceRepository.save(capitalizedIncomeBalance);
|
||||
|
||||
return new CommandProcessingResultBuilder().withLoanId(loan.getId()).withLoanExternalId(loan.getExternalId())
|
||||
.withEntityId(savedCapitalizedIncomeAdjustment.getId())
|
||||
.withEntityExternalId(savedCapitalizedIncomeAdjustment.getExternalId()).build();
|
||||
}
|
||||
|
||||
private void recalculateLoanTransactions(Loan loan, LocalDate transactionDate, LoanTransaction transaction) {
|
||||
if (loan.isInterestBearingAndInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) {
|
||||
reprocessLoanTransactionsService.reprocessTransactions(loan);
|
||||
} else {
|
||||
reprocessLoanTransactionsService.processLatestTransaction(capitalizedIncomeTransaction, loan);
|
||||
reprocessLoanTransactionsService.processLatestTransaction(transaction, loan);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -24,4 +24,6 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionVa
|
||||
public interface ProgressiveLoanTransactionValidator extends LoanTransactionValidator {
|
||||
|
||||
void validateCapitalizedIncome(JsonCommand command, Long loanId);
|
||||
|
||||
void validateCapitalizedIncomeAdjustment(JsonCommand command, Long loanId, Long capitalizedIncomeTransactionId);
|
||||
}
|
||||
|
||||
+81
@@ -27,6 +27,7 @@ import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -35,6 +36,7 @@ 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.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.holiday.domain.Holiday;
|
||||
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
|
||||
import org.apache.fineract.portfolio.common.service.Validator;
|
||||
@@ -45,6 +47,7 @@ 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.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||
@@ -57,6 +60,7 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
|
||||
private final LoanTransactionValidator loanTransactionValidator;
|
||||
private final LoanRepositoryWrapper loanRepositoryWrapper;
|
||||
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
|
||||
@Override
|
||||
public void validateCapitalizedIncome(final JsonCommand command, final Long loanId) {
|
||||
@@ -127,6 +131,78 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateCapitalizedIncomeAdjustment(JsonCommand command, Long loanId, Long capitalizedIncomeTransactionId) {
|
||||
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, getCapitalizedIncomeAdjustmentParameters());
|
||||
|
||||
Validator.validateOrThrow("loan.capitalized.incomeAdjustment", baseDataValidator -> {
|
||||
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
|
||||
validateLoanClientIsActive(loan);
|
||||
validateLoanGroupIsActive(loan);
|
||||
|
||||
// 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();
|
||||
|
||||
Optional<LoanTransaction> capitalizedIncomeTransactionOpt = loanTransactionRepository.findById(capitalizedIncomeTransactionId);
|
||||
if (capitalizedIncomeTransactionOpt.isEmpty()) {
|
||||
baseDataValidator.reset().parameter("capitalizedIncomeTransactionId").failWithCode("loan.transaction.not.found",
|
||||
"Capitalized Income transaction not found.");
|
||||
} else {
|
||||
// Validate not before capitalized income transaction
|
||||
if (transactionDate != null && transactionDate.isBefore(capitalizedIncomeTransactionOpt.get().getTransactionDate())) {
|
||||
baseDataValidator.reset().parameter("transactionDate").failWithCode("before.capitalizedIncome.transaction.date",
|
||||
"Transaction date cannot be before capitalized income transaction date");
|
||||
|
||||
}
|
||||
if (transactionAmount != null) {
|
||||
LoanCapitalizedIncomeBalance capitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository
|
||||
.findByLoanIdAndLoanTransactionId(loanId, capitalizedIncomeTransactionId);
|
||||
if (MathUtil.isLessThan(capitalizedIncomeBalance.getAmount()
|
||||
.subtract(MathUtil.nullToZero(capitalizedIncomeBalance.getAmountAdjustment())), transactionAmount)) {
|
||||
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).failWithCode(
|
||||
"cannot.be.more.than.remaining.amount",
|
||||
" Capitalized income adjustment amount cannot be more than remaining amount");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validatePaymentDetails(baseDataValidator, element);
|
||||
validateNote(baseDataValidator, element);
|
||||
validateExternalId(baseDataValidator, element);
|
||||
});
|
||||
}
|
||||
|
||||
// Delegates
|
||||
@Override
|
||||
public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId) {
|
||||
@@ -280,4 +356,9 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
|
||||
return new HashSet<>(
|
||||
Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId"));
|
||||
}
|
||||
|
||||
private Set<String> getCapitalizedIncomeAdjustmentParameters() {
|
||||
return new HashSet<>(
|
||||
Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId"));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -60,9 +60,10 @@ public class ProgressiveLoanAccountConfiguration {
|
||||
@ConditionalOnMissingBean(ProgressiveLoanTransactionValidator.class)
|
||||
public ProgressiveLoanTransactionValidator progressiveLoanTransactionValidator(FromJsonHelper fromApiJsonHelper,
|
||||
LoanTransactionValidator loanTransactionValidator, LoanRepositoryWrapper loanRepositoryWrapper,
|
||||
LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository) {
|
||||
LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository,
|
||||
LoanTransactionRepository loanTransactionRepository) {
|
||||
return new ProgressiveLoanTransactionValidatorImpl(fromApiJsonHelper, loanTransactionValidator, loanRepositoryWrapper,
|
||||
loanCapitalizedIncomeBalanceRepository);
|
||||
loanCapitalizedIncomeBalanceRepository, loanTransactionRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
Reference in New Issue
Block a user