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