FINERACT-2232: Capitalized income adjustment

This commit is contained in:
adam.magyari
2025-05-29 16:19:29 +02:00
committed by Adam Saghy
parent 94cc68ba7c
commit 82eafad8dd
23 changed files with 672 additions and 10 deletions
@@ -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;
}
}
@@ -0,0 +1,35 @@
/**
* 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.infrastructure.event.business.domain.loan.transaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public class LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent extends LoanTransactionBusinessEvent {
private static final String TYPE = "LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent";
public LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent(LoanTransaction value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
}
@@ -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() {
@@ -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() {
@@ -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;
@@ -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);
}
@@ -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;
@@ -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);
@@ -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();
}
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
@@ -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"));
}
}
@@ -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
@@ -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();
@@ -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();
}
@@ -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 {
@@ -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>
@@ -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>
@@ -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));
@@ -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") //
);
});
}
}
@@ -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;
}
@@ -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)