You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ad...@apache.org on 2025/05/29 14:19:35 UTC
(fineract) 01/03: FINERACT-2232: Capitalized income adjustment
This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
commit 82eafad8dd344706bf1b5ee68235f8c6c899818e
Author: adam.magyari <ma...@gmail.com>
AuthorDate: Tue May 20 11:08:20 2025 +0200
FINERACT-2232: Capitalized income adjustment
---
.../commands/service/CommandWrapperBuilder.java | 9 +
...AdjustmentTransactionCreatedBusinessEvent.java} | 20 ++-
.../loanaccount/data/LoanTransactionEnumData.java | 2 +
.../loanaccount/domain/LoanTransaction.java | 9 +-
.../domain/LoanTransactionRelationTypeEnum.java | 4 +-
.../service/CapitalizedIncomePlatformService.java | 3 +
.../domain/PaymentAllocationTransactionType.java | 3 +-
...dvancedPaymentScheduleTransactionProcessor.java | 2 +-
.../CapitalizedIncomeAdjustmentCommandHandler.java | 53 ++++++
.../LoanCapitalizedIncomeBalanceRepository.java | 3 +
.../CapitalizedIncomeWritePlatformServiceImpl.java | 57 +++++-
.../ProgressiveLoanTransactionValidator.java | 2 +
.../ProgressiveLoanTransactionValidatorImpl.java | 81 +++++++++
.../ProgressiveLoanAccountConfiguration.java | 5 +-
.../AccrualBasedAccountingProcessorForLoan.java | 131 ++++++++++++++
.../api/LoanTransactionsApiResource.java | 2 +
.../loanaccount/api/LoansApiResourceSwagger.java | 2 +
.../db/changelog/tenant/changelog-tenant.xml | 1 +
...d_capitalized_income_adjustment_transaction.xml | 38 ++++
...nalEventConfigurationValidationServiceTest.java | 6 +-
.../LoanCapitalizedIncomeTest.java | 198 +++++++++++++++++++++
.../common/ExternalEventConfigurationHelper.java | 6 +
.../common/loans/LoanTransactionHelper.java | 30 ++++
23 files changed, 649 insertions(+), 18 deletions(-)
diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index a3389825db..872835ffd7 100644
--- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -3831,4 +3831,13 @@ public class CommandWrapperBuilder {
this.href = "/loans/" + loanId;
return this;
}
+
+ public CommandWrapperBuilder capitalizedIncomeAdjustment(final Long loanId, final Long transactionId) {
+ this.actionName = "CAPITALIZEDINCOMEADJUSTMENT";
+ this.entityName = "LOAN";
+ this.entityId = transactionId;
+ this.loanId = loanId;
+ this.href = "/loans/" + loanId + "/transactions/" + transactionId;
+ return this;
+ }
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent.java
similarity index 58%
copy from fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java
copy to fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent.java
index 0c1879323b..ad9ffd961b 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent.java
@@ -16,16 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.portfolio.loanaccount.service;
+package org.apache.fineract.infrastructure.event.business.domain.loan.transaction;
-import org.apache.fineract.infrastructure.core.api.JsonCommand;
-import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
-import org.springframework.transaction.annotation.Transactional;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
-public interface CapitalizedIncomePlatformService {
+public class LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent extends LoanTransactionBusinessEvent {
- @Transactional
- CommandProcessingResult addCapitalizedIncome(Long loanId, JsonCommand command);
+ private static final String TYPE = "LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent";
- void resetBalance(Long loanId);
+ public LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent(LoanTransaction value) {
+ super(value);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
index 573b45703b..5b520eee54 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
@@ -69,6 +69,7 @@ public class LoanTransactionEnumData implements Serializable {
private final boolean accrualAdjustment;
private final boolean capitalizedIncome;
private final boolean capitalizedIncomeAmortization;
+ private final boolean capitalizedIncomeAdjustment;
public LoanTransactionEnumData(final Long id, final String code, final String value) {
this.id = id;
@@ -107,6 +108,7 @@ public class LoanTransactionEnumData implements Serializable {
this.accrualAdjustment = Long.valueOf(LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue()).equals(this.id);
this.capitalizedIncome = Long.valueOf(LoanTransactionType.CAPITALIZED_INCOME.getValue()).equals(this.id);
this.capitalizedIncomeAmortization = Long.valueOf(LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION.getValue()).equals(this.id);
+ this.capitalizedIncomeAdjustment = Long.valueOf(LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT.getValue()).equals(this.id);
}
public boolean isRepaymentType() {
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index 47f4ef5303..80adc30e51 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -322,6 +322,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
};
}
+ public static LoanTransaction capitalizedIncomeAdjustment(final Loan loan, final Money amount, final PaymentDetail paymentDetail,
+ final LocalDate transactionDate, final ExternalId externalId) {
+ return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT, transactionDate,
+ amount.getAmount(), amount.getAmount(), null, null, null, null, false, paymentDetail, externalId);
+ }
+
public LoanTransaction copyTransactionPropertiesAndMappings() {
LoanTransaction newTransaction = copyTransactionProperties(this);
newTransaction.updateLoanTransactionToRepaymentScheduleMappings(loanTransactionToRepaymentScheduleMappings);
@@ -570,7 +576,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
public boolean isRepaymentLikeType() {
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()
- || isChargeAdjustment() || isDownPayment() || isInterestPaymentWaiver() || isInterestRefund();
+ || isChargeAdjustment() || isDownPayment() || isInterestPaymentWaiver() || isInterestRefund()
+ || isCapitalizedIncomeAdjustment();
}
public boolean isTypeAllowedForChargeback() {
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java
index 52cbec85e0..85eae4b28c 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java
@@ -24,7 +24,9 @@ public enum LoanTransactionRelationTypeEnum {
CHARGEBACK(1, "loanTransactionRelationType.chargeback"), //
CHARGE_ADJUSTMENT(2, "loanTransactionRelationType.chargeAdjustment"), //
REPLAYED(3, "loanTransactionRelationType.replayed"), //
- RELATED(4, "loanTransactionRelationType.related");
+ RELATED(4, "loanTransactionRelationType.related"), //
+ ADJUSTMENT(5, "loanTransactionRelationType.adjustment"), //
+ ;
private final Integer value;
private final String code;
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java
index 0c1879323b..b0bacc6d84 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java
@@ -27,5 +27,8 @@ public interface CapitalizedIncomePlatformService {
@Transactional
CommandProcessingResult addCapitalizedIncome(Long loanId, JsonCommand command);
+ @Transactional
+ CommandProcessingResult capitalizedIncomeAdjustment(Long loanId, Long capitalizedIncomeTransactionId, JsonCommand command);
+
void resetBalance(Long loanId);
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java
index 942f3de612..6194a99de2 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java
@@ -42,7 +42,8 @@ public enum PaymentAllocationTransactionType {
CHARGE_PAYMENT(LoanTransactionType.CHARGE_PAYMENT, "Charge payment"), //
REFUND_FOR_ACTIVE_LOAN(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, "Refund for active loan"), //
INTEREST_PAYMENT_WAIVER(LoanTransactionType.INTEREST_PAYMENT_WAIVER, "Interest payment waiver"), //
- INTEREST_REFUND(LoanTransactionType.INTEREST_REFUND, "Interest refund");
+ INTEREST_REFUND(LoanTransactionType.INTEREST_REFUND, "Interest refund"), //
+ CAPITALIZED_INCOME_ADJUSTMENT(LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT, "Capitalized income adjustment");
private final LoanTransactionType loanTransactionType;
private final String humanReadableName;
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index cfbbe223a5..b728e5f977 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -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);
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/CapitalizedIncomeAdjustmentCommandHandler.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/CapitalizedIncomeAdjustmentCommandHandler.java
new file mode 100644
index 0000000000..1815a98812
--- /dev/null
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/CapitalizedIncomeAdjustmentCommandHandler.java
@@ -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();
+ }
+ }
+}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java
index ab5f0f1ac5..0ba8a15fc9 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java
@@ -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);
}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java
index d0e745756e..a931048cd3 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java
@@ -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);
}
}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java
index 192039acd6..0f0d2faa02 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidator.java
@@ -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);
}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java
index b2f306d414..dc3d42e6c6 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java
@@ -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"));
+ }
}
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java
index 31e27793b9..6694b9eab0 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java
@@ -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
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
index 29830b566b..4aa44b87b1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
@@ -21,10 +21,12 @@ package org.apache.fineract.accounting.journalentry.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.domain.GLClosure;
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
@@ -34,11 +36,21 @@ import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
import org.apache.fineract.accounting.journalentry.data.GLAccountBalanceHolder;
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
+import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
+import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
+import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
+import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.PortfolioProductType;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
+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.springframework.stereotype.Component;
@Component
@@ -47,6 +59,9 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
private final AccountingProcessorHelper helper;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
+ private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
+ private final JournalEntryRepository journalEntryRepository;
+ private final LoanTransactionRepository loanTransactionRepository;
@Override
public void createJournalEntriesForLoan(final LoanDTO loanDTO) {
@@ -131,6 +146,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
if (transactionType.isCapitalizedIncomeAmortization()) {
createJournalEntriesForCapitalizedIncomeAmortization(loanDTO, loanTransactionDTO, office);
}
+ // Handle Capitalized Income Adjustment
+ if (transactionType.isCapitalizedIncomeAdjustment()) {
+ createJournalEntriesForCapitalizedIncomeAdjustment(loanDTO, loanTransactionDTO, office);
+ }
}
}
@@ -171,6 +190,118 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
}
+ private void createJournalEntriesForCapitalizedIncomeAdjustment(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
+ final Office office) {
+ // loan properties
+ final Long loanProductId = loanDTO.getLoanProductId();
+ final Long loanId = loanDTO.getLoanId();
+ final String currencyCode = loanDTO.getCurrencyCode();
+ // transaction properties
+ final String transactionId = loanTransactionDTO.getTransactionId();
+ final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
+ final BigDecimal transactionAmount = loanTransactionDTO.getAmount();
+ final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
+ final BigDecimal interestAmount = loanTransactionDTO.getInterest();
+ final BigDecimal feesAmount = loanTransactionDTO.getFees();
+ final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
+ final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment();
+ final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
+ final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
+
+ if (MathUtil.isGreaterThanZero(transactionAmount)) {
+ // Resolve Credit
+ // handle principal payment
+ if (MathUtil.isGreaterThanZero(principalAmount)) {
+ GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
+ AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId);
+ glAccountBalanceHolder.addToCredit(account, principalAmount);
+ }
+ // handle interest payment
+ if (MathUtil.isGreaterThanZero(interestAmount)) {
+ GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
+ AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(), paymentTypeId);
+ glAccountBalanceHolder.addToCredit(account, interestAmount);
+ }
+ // handle fee payment
+ if (MathUtil.isGreaterThanZero(feesAmount)) {
+ GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
+ AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), paymentTypeId);
+ glAccountBalanceHolder.addToCredit(account, feesAmount);
+ }
+ // handle penalty payment
+ if (MathUtil.isGreaterThanZero(penaltiesAmount)) {
+ GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
+ AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), paymentTypeId);
+ glAccountBalanceHolder.addToCredit(account, penaltiesAmount);
+ }
+ // handle overpayment
+ if (MathUtil.isGreaterThanZero(overPaymentAmount)) {
+ GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
+ AccrualAccountsForLoan.OVERPAYMENT.getValue(), paymentTypeId);
+ glAccountBalanceHolder.addToCredit(account, overPaymentAmount);
+ }
+
+ // Resolve Debit
+ GLAccount accountIncomeFromCapitalization = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
+ AccrualAccountsForLoan.INCOME_FROM_CAPITALIZATION.getValue(), paymentTypeId);
+ GLAccount accountDeferredIncome = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
+ AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), paymentTypeId);
+
+ Optional<LoanTransaction> transactionOpt = loanTransactionRepository.findById(Long.parseLong(transactionId));
+ if (transactionOpt.isEmpty()) {
+ throw new PlatformDataIntegrityException("transaction.for.accounting.not.found", "Transaction for accounting not found");
+ }
+ Optional<LoanTransactionRelation> relationToOriginalTransactionOpt = transactionOpt.get().getLoanTransactionRelations().stream()//
+ .filter(rel -> LoanTransactionRelationTypeEnum.REPLAYED.equals(rel.getRelationType()))//
+ .min(Comparator.comparing(AbstractPersistableCustom::getId));
+ if (relationToOriginalTransactionOpt.isPresent()) {
+ // Reverse-replay case
+ final LoanTransaction originalTransaction = relationToOriginalTransactionOpt.get().getToTransaction();
+ final List<JournalEntry> originalJournalEntries = this.journalEntryRepository.findJournalEntries(
+ AccountingProcessorHelper.LOAN_TRANSACTION_IDENTIFIER + originalTransaction.getId(),
+ PortfolioProductType.LOAN.getValue());
+
+ for (JournalEntry originalJournalEntry : originalJournalEntries) {
+ if (originalJournalEntry.isDebitEntry()) {
+ if (accountIncomeFromCapitalization.getId().equals(originalJournalEntry.getGlAccount().getId())) {
+ glAccountBalanceHolder.addToDebit(accountIncomeFromCapitalization, originalJournalEntry.getAmount());
+ } else if (accountDeferredIncome.getId().equals(originalJournalEntry.getGlAccount().getId())) {
+ glAccountBalanceHolder.addToDebit(accountDeferredIncome, originalJournalEntry.getAmount());
+ }
+ }
+ }
+ } else {
+ LoanCapitalizedIncomeBalance capitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository
+ .findBalanceForAdjustment(Long.parseLong(loanTransactionDTO.getTransactionId()));
+ if (MathUtil.isGreaterThan(transactionAmount, capitalizedIncomeBalance.getUnrecognizedAmount())) {
+ BigDecimal amortizedAmount = transactionAmount.subtract(capitalizedIncomeBalance.getUnrecognizedAmount());
+
+ glAccountBalanceHolder.addToDebit(accountIncomeFromCapitalization, amortizedAmount);
+ glAccountBalanceHolder.addToDebit(accountDeferredIncome, transactionAmount.subtract(amortizedAmount));
+ } else {
+ glAccountBalanceHolder.addToDebit(accountDeferredIncome, transactionAmount);
+ }
+ }
+ }
+
+ // create credit entries
+ for (Map.Entry<Long, BigDecimal> creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) {
+ if (MathUtil.isGreaterThanZero(creditEntry.getValue())) {
+ GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey());
+ this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
+ creditEntry.getValue(), glAccount);
+ }
+ }
+ // create debit entries
+ for (Map.Entry<Long, BigDecimal> debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) {
+ if (MathUtil.isGreaterThanZero(debitEntry.getValue())) {
+ GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey());
+ this.helper.createDebitJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
+ debitEntry.getValue(), glAccount);
+ }
+ }
+ }
+
private void createJournalEntriesForCapitalizedIncomeAmortization(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office) {
final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
index 89a1c58983..33e9da65e2 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
@@ -700,6 +700,8 @@ public class LoanTransactionsApiResource {
CommandWrapper commandRequest;
if (CommandParameterUtil.is(commandParam, LoanApiConstants.CHARGEBACK_TRANSACTION_COMMAND)) {
commandRequest = builder.chargebackTransaction(resolvedLoanId, resolvedTransactionId).build();
+ } else if (CommandParameterUtil.is(commandParam, LoanApiConstants.CAPITALIZED_INCOME_ADJUSTMENT_TRANSACTION_COMMAND)) {
+ commandRequest = builder.capitalizedIncomeAdjustment(resolvedLoanId, resolvedTransactionId).build();
} else { // Default to adjust the Loan Transaction
commandRequest = builder.adjustTransaction(resolvedLoanId, resolvedTransactionId).build();
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
index dfc38b76f7..39a37a81de 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
@@ -730,6 +730,8 @@ final class LoansApiResourceSwagger {
public boolean capitalizedIncome;
@Schema(example = "false")
public boolean capitalizedIncomeAmortization;
+ @Schema(example = "false")
+ public boolean capitalizedIncomeAdjustment;
}
static final class GetLoansLoanIdPaymentDetailData {
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index af37175c51..ce67fcb5da 100644
--- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -197,4 +197,5 @@
<include file="parts/0176_add_capitalized_income_amortization_transaction.xml" relativeToChangelogFile="true" />
<include file="parts/0177_acc_journal_entry_index.xml" relativeToChangelogFile="true" />
<include file="parts/0178_add_principal_from_capitalized_income_to_loan_summary.xml" relativeToChangelogFile="true" />
+ <include file="parts/0179_add_capitalized_income_adjustment_transaction.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0179_add_capitalized_income_adjustment_transaction.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0179_add_capitalized_income_adjustment_transaction.xml
new file mode 100644
index 0000000000..6005567757
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0179_add_capitalized_income_adjustment_transaction.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+ <changeSet id="1" author="fineract">
+ <insert tableName="m_external_event_configuration">
+ <column name="type" value="LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ <insert tableName="r_enum_value">
+ <column name="enum_name" value="transaction_type_enum"/>
+ <column name="enum_id" valueNumeric="37"/>
+ <column name="enum_message_property" value="Capitalized Income Adjustment"/>
+ <column name="enum_value" value="Capitalized Income Adjustment"/>
+ <column name="enum_type" valueBoolean="false"/>
+ </insert>
+ </changeSet>
+</databaseChangeLog>
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
index 00e59f0afd..141e406ac8 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
@@ -106,7 +106,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
"LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent",
"LoanTransactionInterestRefundPreBusinessEvent", "LoanAccrualAdjustmentTransactionBusinessEvent",
- "LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent");
+ "LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
+ "LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -193,7 +194,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
"LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent",
"LoanTransactionInterestRefundPreBusinessEvent", "LoanAccrualAdjustmentTransactionBusinessEvent",
- "LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent");
+ "LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
+ "LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java
index 8d96e0b5ef..7e5ad02f8a 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java
@@ -19,12 +19,17 @@
package org.apache.fineract.integrationtests;
import java.math.BigDecimal;
+import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.integrationtests.common.BusinessStepHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -96,4 +101,197 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
);
});
}
+
+ @Test
+ public void testLoanCapitalizedIncomeAdjustment() {
+ final AtomicReference<Long> loanIdRef = new AtomicReference<>();
+ final AtomicReference<Long> capitalizedIncomeIdRef = new AtomicReference<>();
+
+ final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+
+ final PostLoanProductsResponse loanProductsResponse = loanProductHelper
+ .createLoanProduct(create4IProgressive().enableIncomeCapitalization(true)
+ .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)
+ .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)
+ .deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())
+ .incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue())
+ .capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE));
+
+ runAt("1 April 2024", () -> {
+ Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
+ 500.0, 7.0, 3, null);
+ loanIdRef.set(loanId);
+
+ disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
+ PostLoansLoanIdTransactionsResponse capitalizedIncomeResponse = loanTransactionHelper.addCapitalizedIncome(loanId,
+ "1 January 2024", 50.0);
+ capitalizedIncomeIdRef.set(capitalizedIncomeResponse.getResourceId());
+
+ loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "1 April 2024", 50.0);
+
+ verifyTransactions(loanId, //
+ transaction(100.0, "Disbursement", "01 January 2024"), //
+ transaction(50.0, "Capitalized Income", "01 January 2024"), //
+ transaction(50.0, "Capitalized Income Adjustment", "01 April 2024") //
+ );
+
+ verifyJournalEntries(loanId, //
+ journalEntry(100, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100, fundSource, "CREDIT"), //
+ journalEntry(50, loansReceivableAccount, "DEBIT"), //
+ journalEntry(50, deferredIncomeLiabilityAccount, "CREDIT"), //
+ journalEntry(50.0, deferredIncomeLiabilityAccount, "DEBIT"), //
+ journalEntry(49.71, loansReceivableAccount, "CREDIT"), //
+ journalEntry(0.29, interestReceivableAccount, "CREDIT") //
+ );
+ });
+ }
+
+ @Test
+ public void testLoanCapitalizedIncomeAdjustmentValidations() {
+ final AtomicReference<Long> loanIdRef = new AtomicReference<>();
+ final AtomicReference<Long> capitalizedIncomeIdRef = new AtomicReference<>();
+
+ final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+
+ final PostLoanProductsResponse loanProductsResponse = loanProductHelper
+ .createLoanProduct(create4IProgressive().enableIncomeCapitalization(true)
+ .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)
+ .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)
+ .deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())
+ .incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue())
+ .capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE));
+
+ runAt("3 January 2024", () -> {
+ Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
+ 500.0, 7.0, 3, null);
+ loanIdRef.set(loanId);
+
+ disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
+ PostLoansLoanIdTransactionsResponse capitalizedIncomeResponse = loanTransactionHelper.addCapitalizedIncome(loanId,
+ "3 January 2024", 50.0);
+ capitalizedIncomeIdRef.set(capitalizedIncomeResponse.getResourceId());
+
+ // Amount more than remaining
+ Assertions.assertThrows(RuntimeException.class,
+ () -> loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "3 January 2024", 60.0));
+
+ loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "3 January 2024", 30.0);
+ Assertions.assertThrows(RuntimeException.class,
+ () -> loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "3 January 2024", 30.0));
+
+ // Capitalized income transaction with given id doesn't exist for this loan
+ Assertions.assertThrows(RuntimeException.class,
+ () -> loanTransactionHelper.capitalizedIncomeAdjustment(loanId, 1L, "3 January 2024", 30.0));
+
+ // Cannot be earlier than capitalized income transaction
+ Assertions.assertThrows(RuntimeException.class,
+ () -> loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "2 January 2024", 30.0));
+ });
+ }
+
+ @Test
+ public void testLoanCapitalizedIncomeAdjustmentWithAmortizationAccounting() {
+ final AtomicReference<Long> loanIdRef = new AtomicReference<>();
+ final AtomicReference<Long> capitalizedIncomeIdRef = new AtomicReference<>();
+
+ final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+
+ final PostLoanProductsResponse loanProductsResponse = loanProductHelper
+ .createLoanProduct(create4IProgressive().enableIncomeCapitalization(true)
+ .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)
+ .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)
+ .deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())
+ .incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue())
+ .capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE));
+
+ runAt("1 January 2024", () -> {
+ Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
+ 500.0, 7.0, 3, null);
+ loanIdRef.set(loanId);
+
+ disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
+ PostLoansLoanIdTransactionsResponse capitalizedIncomeResponse = loanTransactionHelper.addCapitalizedIncome(loanId,
+ "1 January 2024", 100.0);
+ capitalizedIncomeIdRef.set(capitalizedIncomeResponse.getResourceId());
+ });
+ runAt("2 January 2024", () -> {
+ Long loanId = loanIdRef.get();
+ executeInlineCOB(loanId);
+
+ verifyTransactions(loanId, //
+ transaction(100.0, "Disbursement", "01 January 2024"), //
+ transaction(100.0, "Capitalized Income", "01 January 2024"), //
+ transaction(1.10, "Capitalized Income Amortization", "01 January 2024") //
+ );
+ });
+ runAt("3 January 2024", () -> {
+ Long loanId = loanIdRef.get();
+ executeInlineCOB(loanId);
+
+ verifyTransactions(loanId, //
+ transaction(100.0, "Disbursement", "01 January 2024"), //
+ transaction(100.0, "Capitalized Income", "01 January 2024"), //
+ transaction(1.10, "Capitalized Income Amortization", "01 January 2024"), //
+ transaction(0.04, "Accrual", "02 January 2024"), //
+ transaction(1.10, "Capitalized Income Amortization", "02 January 2024") //
+ );
+
+ verifyJournalEntries(loanId, //
+ journalEntry(100, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100, fundSource, "CREDIT"), //
+ journalEntry(100, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100, deferredIncomeLiabilityAccount, "CREDIT"), //
+ journalEntry(1.10, deferredIncomeLiabilityAccount, "DEBIT"), //
+ journalEntry(1.10, feeIncomeAccount, "CREDIT"), //
+ journalEntry(0.04, interestReceivableAccount, "DEBIT"), //
+ journalEntry(0.04, interestIncomeAccount, "CREDIT"), //
+ journalEntry(1.10, deferredIncomeLiabilityAccount, "DEBIT"), //
+ journalEntry(1.10, feeIncomeAccount, "CREDIT") //
+ );
+
+ loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "3 January 2024", 100.0);
+
+ verifyTransactions(loanId, //
+ transaction(100.0, "Disbursement", "01 January 2024"), //
+ transaction(100.0, "Capitalized Income", "01 January 2024"), //
+ transaction(1.10, "Capitalized Income Amortization", "01 January 2024"), //
+ transaction(0.04, "Accrual", "02 January 2024"), //
+ transaction(1.10, "Capitalized Income Amortization", "02 January 2024"), //
+ transaction(100.0, "Capitalized Income Adjustment", "03 January 2024") //
+ );
+
+ verifyJournalEntries(loanId, //
+ journalEntry(100, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100, fundSource, "CREDIT"), //
+ journalEntry(100, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100, deferredIncomeLiabilityAccount, "CREDIT"), //
+ journalEntry(1.10, deferredIncomeLiabilityAccount, "DEBIT"), //
+ journalEntry(1.10, feeIncomeAccount, "CREDIT"), //
+ journalEntry(0.04, interestReceivableAccount, "DEBIT"), //
+ journalEntry(0.04, interestIncomeAccount, "CREDIT"), //
+ journalEntry(1.10, deferredIncomeLiabilityAccount, "DEBIT"), //
+ journalEntry(1.10, feeIncomeAccount, "CREDIT"), //
+ journalEntry(99.92, loansReceivableAccount, "CREDIT"), //
+ journalEntry(0.08, interestReceivableAccount, "CREDIT"), //
+ journalEntry(2.20, feeIncomeAccount, "DEBIT"), //
+ journalEntry(97.80, deferredIncomeLiabilityAccount, "DEBIT") //
+ );
+
+ // Reverse-replay
+ addRepaymentForLoan(loanId, 67.45, "2 January 2024");
+
+ GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ Optional<GetLoansLoanIdTransactions> replayedCapitalizedIncomeAdjustmentOpt = loanDetails.getTransactions().stream()
+ .filter(t -> t.getType().getCapitalizedIncomeAdjustment()).findFirst();
+ Assertions.assertTrue(replayedCapitalizedIncomeAdjustmentOpt.isPresent(), "Capitalized income adjustment not found");
+
+ verifyTRJournalEntries(replayedCapitalizedIncomeAdjustmentOpt.get().getId(), //
+ journalEntry(99.98, loansReceivableAccount, "CREDIT"), //
+ journalEntry(0.02, interestReceivableAccount, "CREDIT"), //
+ journalEntry(2.20, feeIncomeAccount, "DEBIT"), //
+ journalEntry(97.80, deferredIncomeLiabilityAccount, "DEBIT") //
+ );
+ });
+ }
}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
index 03f91075df..8682ba92ed 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
@@ -589,6 +589,12 @@ public class ExternalEventConfigurationHelper {
loanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent.put("enabled", false);
defaults.add(loanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent);
+ Map<String, Object> loanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent = new HashMap<>();
+ loanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent.put("type",
+ "LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent");
+ loanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent.put("enabled", false);
+ defaults.add(loanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent);
+
return defaults;
}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index b66700b9d6..da5059b6c8 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -933,6 +933,36 @@ public class LoanTransactionHelper {
new InterestPauseRequestDto().startDate(startDate).endDate(endDate).dateFormat(DATE_FORMAT).locale("en")));
}
+ public PostLoansLoanIdTransactionsResponse capitalizedIncomeAdjustment(final Long loanId, final Long capitalizedIncomeTransactionId,
+ final PostLoansLoanIdTransactionsTransactionIdRequest request) {
+ return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction(loanId,
+ capitalizedIncomeTransactionId, request, "capitalizedIncomeAdjustment"));
+ }
+
+ public PostLoansLoanIdTransactionsResponse capitalizedIncomeAdjustment(final String loanExternalId, final Long transactionId,
+ final PostLoansLoanIdTransactionsTransactionIdRequest request) {
+ return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction2(loanExternalId, transactionId,
+ request, "capitalizedIncomeAdjustment"));
+ }
+
+ public PostLoansLoanIdTransactionsResponse capitalizedIncomeAdjustment(final String loanExternalId, final String transactionExternalId,
+ final PostLoansLoanIdTransactionsTransactionIdRequest request) {
+ return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction3(loanExternalId,
+ transactionExternalId, request, "capitalizedIncomeAdjustment"));
+ }
+
+ public PostLoansLoanIdTransactionsResponse capitalizedIncomeAdjustment(final Long loanId, final String transactionExternalId,
+ final PostLoansLoanIdTransactionsTransactionIdRequest request) {
+ return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction1(loanId, transactionExternalId,
+ request, "capitalizedIncomeAdjustment"));
+ }
+
+ public PostLoansLoanIdTransactionsResponse capitalizedIncomeAdjustment(final Long loanId, final Long capitalizedIncomeTransactionId,
+ final String transactionDate, final double amount) {
+ return capitalizedIncomeAdjustment(loanId, capitalizedIncomeTransactionId, new PostLoansLoanIdTransactionsTransactionIdRequest()
+ .transactionAmount(amount).transactionDate(transactionDate).dateFormat("dd MMMM yyyy").locale("en"));
+ }
+
// TODO: Rewrite to use fineract-client instead!
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)