From d0342c08e413f51c1eafa4fc8e9d19969100446d Mon Sep 17 00:00:00 2001 From: Marta Jankovics Date: Mon, 11 Nov 2024 13:42:58 +0100 Subject: [PATCH] FINERACT-2060: Accrual reverse replay logic and Handling --- .../service/ChargeReadPlatformService.java | 3 +- .../infrastructure/core/service/MathUtil.java | 15 + ...ualAdjustmentTransactionBusinessEvent.java | 35 + .../loanaccount/data/AccrualAmountsData.java | 87 ++ .../loanaccount/data/AccrualChargeData.java | 38 + .../loanaccount/data/LoanChargeData.java | 18 +- .../data/LoanTransactionEnumData.java | 2 + .../portfolio/loanaccount/domain/Loan.java | 43 +- .../loanaccount/domain/LoanCharge.java | 70 +- .../LoanInterestRecalculationDetails.java | 2 +- ...oanRepaymentScheduleProcessingWrapper.java | 9 + .../loanaccount/domain/LoanRepository.java | 13 + .../domain/LoanRepositoryWrapper.java | 4 + .../loanaccount/domain/LoanTransaction.java | 84 +- ...TransactionToRepaymentScheduleMapping.java | 10 + .../domain/LoanTransactionType.java | 5 + ...rgeRepaymentScheduleProcessingWrapper.java | 25 +- ...stractCumulativeLoanScheduleGenerator.java | 27 + .../domain/LoanScheduleGenerator.java | 4 + .../LoanAccrualsProcessingService.java | 20 +- .../loanaccount/service/LoanAssembler.java | 50 + .../LoanChargeReadPlatformService.java | 5 +- .../loanproduct/service/LoanEnumerations.java | 2 + ...edPaymentScheduleTransactionProcessor.java | 578 +++++----- .../loanschedule/data/InterestPeriod.java | 5 +- .../ProgressiveLoanInterestScheduleModel.java | 25 +- .../loanschedule/data/RepaymentPeriod.java | 52 +- .../ProgressiveLoanScheduleGenerator.java | 108 +- .../loanproduct/calc/EMICalculator.java | 16 +- .../calc/ProgressiveEMICalculator.java | 134 +-- ...ymentScheduleTransactionProcessorTest.java | 4 +- .../calc/ProgressiveEMICalculatorTest.java | 105 +- ...ccrualBasedAccountingProcessorForLoan.java | 56 +- .../ChargeReadPlatformServiceImpl.java | 2 +- .../api/LoanChargesApiResource.java | 5 +- .../AddAccrualEntriesTasklet.java | 6 +- .../LoanAccrualsProcessingServiceImpl.java | 1003 +++++++++-------- ...nAssembler.java => LoanAssemblerImpl.java} | 12 +- .../LoanChargeReadPlatformServiceImpl.java | 21 +- .../service/LoanReadPlatformService.java | 8 +- .../service/LoanReadPlatformServiceImpl.java | 12 +- ...gressiveLoanInterestRefundServiceImpl.java | 19 +- .../starter/LoanAccountAutoStarter.java | 7 +- .../starter/LoanAccountConfiguration.java | 3 +- ...ansaction_external_event_configuration.xml | 6 + .../loanaccount/domain/LoanTest.java | 13 +- 46 files changed, 1568 insertions(+), 1203 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanAccrualAdjustmentTransactionBusinessEvent.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualAmountsData.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualChargeData.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java rename fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/{LoanAssembler.java => LoanAssemblerImpl.java} (99%) diff --git a/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java b/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java index fbec2a84fc3..5030f2912cc 100644 --- a/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java +++ b/fineract-charge/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformService.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.charge.service; import java.util.Collection; +import java.util.List; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; @@ -53,7 +54,7 @@ public interface ChargeReadPlatformService { * Excludes Given List of Charge Types from the response * @return */ - Collection retrieveLoanAccountApplicableCharges(Long loanId, ChargeTimeType[] excludeChargeTimes); + List retrieveLoanAccountApplicableCharges(Long loanId, ChargeTimeType[] excludeChargeTimes); /** * Returns all charges applicable for a given loan product (filter based on Currency of Selected Loan Product) diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java index 4e2178d9a99..b874e5f74b9 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java @@ -291,6 +291,11 @@ public static BigDecimal subtract(BigDecimal first, BigDecimal second) { return subtract(first, second, MoneyHelper.getMathContext()); } + /** @return first minus the others considering null values, maybe negative */ + public static BigDecimal subtract(BigDecimal first, BigDecimal second, BigDecimal third) { + return subtract(subtract(first, second), third); + } + /** @return first minus second considering null values, maybe negative */ public static BigDecimal subtract(BigDecimal first, BigDecimal second, MathContext mc) { return first == null ? null : second == null ? first : first.subtract(second, mc); @@ -454,6 +459,16 @@ public static Money min(Money first, Money second, Money third, boolean notNull) return min(min(first, second, notNull), third, notNull); } + /** @return Money null safe negate */ + public static Money negate(Money amount) { + return negate(amount, MoneyHelper.getMathContext()); + } + + /** @return Money null safe negate */ + public static Money negate(Money amount, MathContext mc) { + return isEmpty(amount) ? amount : amount.negated(mc); + } + /** * Calculate percentage of a value * diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanAccrualAdjustmentTransactionBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanAccrualAdjustmentTransactionBusinessEvent.java new file mode 100644 index 00000000000..73cb3619248 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanAccrualAdjustmentTransactionBusinessEvent.java @@ -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 LoanAccrualAdjustmentTransactionBusinessEvent extends LoanTransactionBusinessEvent { + + private static final String TYPE = "LoanAccrualAdjustmentTransactionBusinessEvent"; + + public LoanAccrualAdjustmentTransactionBusinessEvent(LoanTransaction value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualAmountsData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualAmountsData.java new file mode 100644 index 00000000000..77b6809c89c --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualAmountsData.java @@ -0,0 +1,87 @@ +/** + * 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.data; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; + +@Data +@Accessors(chain = true) +@RequiredArgsConstructor +public class AccrualAmountsData { + + private final Integer installmentNumber; + private final MonetaryCurrency currency; + private Money interestAmount; + private Money interestAccruable; + private Money interestAccrued; + private List charges; + + public AccrualAmountsData addCharge(AccrualChargeData charge) { + if (charges == null) { + charges = new ArrayList<>(); + } + charges.add(charge); + return this; + } + + public Money getChargeAmount() { + return charges.stream().map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus); + } + + public Money getFeeAmount() { + return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus); + } + + public Money getPenaltyAmount() { + return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus); + } + + public Money getChargeAccrued() { + return charges.stream().map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus); + } + + public Money getFeeAccrued() { + return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus); + } + + public Money getPenaltyAccrued() { + return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus); + } + + public Money getChargeAccruable() { + return charges.stream().map(AccrualChargeData::getChargeAccruable).reduce(null, MathUtil::plus); + } + + public Money getFeeAccruable() { + return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAccruable).reduce(null, + MathUtil::plus); + } + + public Money getPenaltyAccruable() { + return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAccruable).reduce(null, + MathUtil::plus); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualChargeData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualChargeData.java new file mode 100644 index 00000000000..a4798fcda39 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccrualChargeData.java @@ -0,0 +1,38 @@ +/** + * 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.data; + +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; +import org.apache.fineract.organisation.monetary.domain.Money; + +@Data +@Accessors(chain = true) +@RequiredArgsConstructor +public class AccrualChargeData { + + private final Long loanChargeId; + private final Long loanInstallmentChargeId; + private final boolean isPenalty; + private Money chargeAmount; + private Money chargeAccruable; + private Money chargeAccrued; + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java index 5ed0085b5c9..4c9a4c5bdb7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanChargeData.java @@ -20,7 +20,7 @@ import java.math.BigDecimal; import java.time.LocalDate; -import java.util.Collection; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -66,7 +66,7 @@ public class LoanChargeData { private final BigDecimal amountOrPercentage; - private final Collection chargeOptions; + private final List chargeOptions; private final boolean penalty; @@ -84,7 +84,7 @@ public class LoanChargeData { private final BigDecimal maxCap; - private final Collection installmentChargeData; + private final List installmentChargeData; private BigDecimal amountAccrued; @@ -94,7 +94,7 @@ public class LoanChargeData { private final ExternalId externalLoanId; - public static LoanChargeData template(final Collection chargeOptions) { + public static LoanChargeData template(final List chargeOptions) { return new LoanChargeData(null, null, null, null, null, null, null, null, chargeOptions, false, null, false, false, null, ExternalId.empty(), null, null, null, null, ExternalId.empty()); } @@ -116,7 +116,7 @@ public LoanChargeData(final Long id, final Long chargeId, final String name, fin final LocalDate dueDate, final EnumOptionData chargeCalculationType, final BigDecimal percentage, final BigDecimal amountPercentageAppliedTo, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid, final boolean waived, final Long loanId, final ExternalId externalLoanId, final BigDecimal minCap, final BigDecimal maxCap, - final BigDecimal amountOrPercentage, Collection installmentChargeData, final ExternalId externalId) { + final BigDecimal amountOrPercentage, List installmentChargeData, final ExternalId externalId) { this.id = id; this.chargeId = chargeId; this.name = name; @@ -160,9 +160,9 @@ public LoanChargeData(final Long id, final Long chargeId, final String name, fin private LoanChargeData(final Long id, final Long chargeId, final String name, final CurrencyData currency, final BigDecimal amount, final BigDecimal percentage, final EnumOptionData chargeTimeType, final EnumOptionData chargeCalculationType, - final Collection chargeOptions, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid, + final List chargeOptions, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid, final boolean waived, final Long loanId, final ExternalId externalLoanId, final BigDecimal minCap, final BigDecimal maxCap, - final BigDecimal amountOrPercentage, Collection installmentChargeData, final ExternalId externalId) { + final BigDecimal amountOrPercentage, List installmentChargeData, final ExternalId externalId) { this.id = id; this.chargeId = chargeId; this.name = name; @@ -207,7 +207,7 @@ private LoanChargeData(final Long id, final Long chargeId, final String name, fi public LoanChargeData(final Long id, final LocalDate dueAsOfDate, final LocalDate submittedOnDate, final BigDecimal amountOutstanding, EnumOptionData chargeTimeType, final Long loanId, final ExternalId externalLoanId, - Collection installmentChargeData, final ExternalId externalId) { + List installmentChargeData, final ExternalId externalId) { this.id = id; this.chargeId = null; this.name = null; @@ -308,7 +308,7 @@ public LoanChargeData(final BigDecimal amountUnrecognized, final LoanChargeData this.externalId = chargeData.externalId; } - public LoanChargeData(LoanChargeData chargeData, Collection installmentChargeData) { + public LoanChargeData(LoanChargeData chargeData, List installmentChargeData) { this.id = chargeData.id; this.chargeId = chargeData.chargeId; this.name = chargeData.name; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java index b3e34c74446..82b95d667a9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java @@ -61,6 +61,7 @@ public class LoanTransactionEnumData { private final boolean reAmortize; private final boolean accrualActivity; private final boolean interestRefund; + private final boolean accrualAdjustment; public LoanTransactionEnumData(final Long id, final String code, final String value) { this.id = id; @@ -96,6 +97,7 @@ public LoanTransactionEnumData(final Long id, final String code, final String va this.reAge = Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id); this.reAmortize = Long.valueOf(LoanTransactionType.REAMORTIZE.getValue()).equals(this.id); this.interestRefund = Long.valueOf(LoanTransactionType.INTEREST_REFUND.getValue()).equals(this.id); + this.accrualAdjustment = Long.valueOf(LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue()).equals(this.id); } public boolean isRepaymentType() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index f5723539039..0bd06e8a51a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -3567,7 +3567,7 @@ public Money getReceivableInterest(final LocalDate tillDate) { && !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) { if (transaction.isAccrual()) { receivableInterest = receivableInterest.plus(transaction.getInterestPortion(getCurrency())); - } else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver()) { + } else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver() || transaction.isAccrualAdjustment()) { receivableInterest = receivableInterest.minus(transaction.getInterestPortion(getCurrency())); } } @@ -3765,10 +3765,6 @@ public void updateInterestRateFrequencyType() { this.loanRepaymentScheduleDetail.setInterestPeriodFrequencyType(this.loanProduct.getInterestPeriodFrequencyType()); } - public void updateInterestRateFrequencyType(PeriodFrequencyType periodFrequencyType) { - this.loanRepaymentScheduleDetail.setInterestPeriodFrequencyType(periodFrequencyType); - } - public void addLoanTransaction(final LoanTransaction loanTransaction) { this.loanTransactions.add(loanTransaction); } @@ -3893,8 +3889,7 @@ public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final LoanTra public LocalDate getLastUserTransactionDate() { LocalDate currentTransactionDate = getDisbursementDate(); for (final LoanTransaction previousTransaction : this.loanTransactions) { - if (!(previousTransaction.isReversed() || previousTransaction.isAccrual() || previousTransaction.isIncomePosting() - || previousTransaction.isAccrualActivity()) + if (!(previousTransaction.isReversed() || previousTransaction.isAccrualRelated() || previousTransaction.isIncomePosting()) && DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate())) { currentTransactionDate = previousTransaction.getTransactionDate(); } @@ -4585,6 +4580,16 @@ public LoanRepaymentScheduleInstallment getRepaymentScheduleInstallment( return getRepaymentScheduleInstallments().stream().filter(predicate).findFirst().orElse(null); } + /** + * @param predicate + * filter of the installments + * @return the installments matching the filter + **/ + public List getRepaymentScheduleInstallments( + @NotNull Predicate predicate) { + return getRepaymentScheduleInstallments().stream().filter(predicate).toList(); + } + /** * @return loan disbursement data **/ @@ -4743,17 +4748,18 @@ public Boolean shouldCreateStandingInstructionAtDisbursement() { return this.createStandingInstructionAtDisbursement != null && this.createStandingInstructionAtDisbursement; } - public Collection getLoanCharges(LocalDate dueDate) { - Collection loanCharges = new ArrayList<>(); - - for (LoanCharge loanCharge : charges) { - - if (loanCharge.getDueLocalDate() != null && loanCharge.getDueLocalDate().equals(dueDate)) { - loanCharges.add(loanCharge); - } - } + public List getLoanChargesByDueDate(LocalDate dueDate) { + return getLoanCharges( + loanCharge -> loanCharge.getDueLocalDate() != null && DateUtils.isEqual(loanCharge.getDueLocalDate(), dueDate)); + } - return loanCharges; + /** + * @param predicate + * filter of the charges + * @return the loan charges matching the filter + **/ + public List getLoanCharges(@NotNull Predicate predicate) { + return getLoanCharges().stream().filter(predicate).toList(); } public void setGuaranteeAmount(BigDecimal guaranteeAmountDerived) { @@ -5508,8 +5514,7 @@ public void handleMaturityDateActivate() { public LoanTransaction getLastUserTransaction() { return getLoanTransactions().stream() // - .filter(LoanTransaction::isNotReversed) // - .filter(t -> !(t.isAccrualTransaction() || t.isIncomePosting())) // + .filter(t -> t.isNotReversed() && !(t.isAccrual() || t.isAccrualAdjustment() || t.isIncomePosting())) // .reduce((first, second) -> second) // .orElse(null); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java index 9e3dcfdb1b7..9058b7f778c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java @@ -28,6 +28,7 @@ import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; @@ -40,7 +41,8 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; +import java.util.function.Predicate; +import lombok.Getter; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; @@ -58,6 +60,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail; import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData; +@Getter @Entity @Table(name = "m_loan_charge", uniqueConstraints = { @UniqueConstraint(columnNames = { "external_id" }, name = "external_id") }) public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { @@ -305,10 +308,6 @@ public Money waive(final MonetaryCurrency currency, final Integer loanInstallmen } - public BigDecimal getAmountPercentageAppliedTo() { - return this.amountPercentageAppliedTo; - } - private BigDecimal calculateAmountOutstanding(final MonetaryCurrency currency) { return getAmount(currency).minus(getAmountWaived(currency)).minus(getAmountPaid(currency)).getAmount(); } @@ -509,15 +508,7 @@ private static boolean isGreaterThanZero(final BigDecimal value) { } public LocalDate getDueLocalDate() { - return this.dueDate; - } - - public LocalDate getDueDate() { - return this.dueDate; - } - - public LocalDate getSubmittedOnDate() { - return submittedOnDate; + return this.dueDate; // TODO delete duplicated method } private boolean determineIfFullyPaid() { @@ -597,11 +588,11 @@ private BigDecimal minimumAndMaximumCap(final BigDecimal percentageOf) { } public BigDecimal amount() { - return this.amount; + return this.amount; // TODO delete duplicated method } public BigDecimal amountOutstanding() { - return this.amountOutstanding; + return this.amountOutstanding; // TODO delete duplicated method } public Money getAmountOutstanding(final MonetaryCurrency currency) { @@ -644,14 +635,6 @@ public boolean isWaived() { return this.waived; } - public BigDecimal getMinCap() { - return this.minCap; - } - - public BigDecimal getMaxCap() { - return this.maxCap; - } - public boolean isPaidOrPartiallyPaid(final MonetaryCurrency currency) { final Money amountWaivedOrWrittenOff = getAmountWaived(currency).plus(getAmountWrittenOff(currency)); @@ -729,10 +712,6 @@ public String currencyCode() { return this.charge.getCurrencyCode(); } - public Charge getCharge() { - return this.charge; - } - /* * @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (obj == this) { return * true; } if (obj.getClass() != getClass()) { return false; } final LoanCharge rhs = (LoanCharge) obj; return new @@ -756,10 +735,6 @@ public ChargeCalculationType getChargeCalculation() { return ChargeCalculationType.fromInt(this.chargeCalculation); } - public BigDecimal getPercentage() { - return this.percentage; - } - public void updateAmount(final BigDecimal amount) { this.amount = amount; calculateOutstanding(); @@ -851,7 +826,7 @@ public void setActive(boolean active) { } public BigDecimal amountOrPercentage() { - return this.amountOrPercentage; + return this.amountOrPercentage; // TODO delete duplicated method } public BigDecimal chargeAmount() { @@ -906,12 +881,8 @@ public void updateWaivedAmount(MonetaryCurrency currency) { } - public LoanOverdueInstallmentCharge getOverdueInstallmentCharge() { - return this.overdueInstallmentCharge; - } - public LoanTrancheDisbursementCharge getTrancheDisbursementCharge() { - return this.loanTrancheDisbursementCharge; + return this.loanTrancheDisbursementCharge; // TODO delete duplicated method } public Money undoPaidOrPartiallyAmountBy(final Money incrementBy, final Integer installmentNumber, final Money feeAmount) { @@ -960,14 +931,6 @@ public LoanInstallmentCharge getLastPaidOrPartiallyPaidInstallmentLoanCharge(Mon return paidChargePerInstallment; } - public Set getLoanChargePaidBySet() { - return this.loanChargePaidBySet; - } - - public Loan getLoan() { - return this.loan; - } - public boolean isDisbursementCharge() { return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT); } @@ -988,10 +951,6 @@ public void undoWaived() { this.waived = false; } - public ExternalId getExternalId() { - return externalId; - } - public ChargeTimeType getChargeTimeType() { return ChargeTimeType.fromInt(this.chargeTime); } @@ -1013,6 +972,11 @@ public LocalDate getEffectiveDueDate() { return dueDate; } + @NotNull + public List getLoanChargePaidBy(@NotNull Predicate filter) { + return getLoanChargePaidBySet().stream().filter(filter).toList(); + } + public LoanChargeData toData() { EnumOptionData chargeTimeTypeData = new EnumOptionData((long) getChargeTimeType().ordinal(), getChargeTimeType().getCode(), String.valueOf(getChargeTimeType().getValue())); @@ -1020,8 +984,8 @@ public LoanChargeData toData() { getChargeCalculation().getCode(), String.valueOf(getChargeCalculation().getValue())); EnumOptionData chargePaymentModeData = new EnumOptionData((long) getChargePaymentMode().ordinal(), getChargePaymentMode().getCode(), String.valueOf(getChargePaymentMode().getValue())); - Set loanInstallmentChargeDataSet = installmentCharges().stream().map(LoanInstallmentCharge::toData) - .collect(Collectors.toSet()); + List loanInstallmentChargeDataList = installmentCharges().stream().map(LoanInstallmentCharge::toData) + .toList(); return LoanChargeData.builder().id(getId()).chargeId(getCharge().getId()).name(getCharge().getName()) .currency(getCharge().toData().getCurrency()).amount(amount).amountPaid(amountPaid).amountWaived(amountWaived) @@ -1029,6 +993,6 @@ public LoanChargeData toData() { .submittedOnDate(submittedOnDate).dueDate(dueDate).chargeCalculationType(chargeCalculationTypeData).percentage(percentage) .amountPercentageAppliedTo(amountPercentageAppliedTo).amountOrPercentage(amountOrPercentage).penalty(penaltyCharge) .chargePaymentMode(chargePaymentModeData).paid(paid).waived(waived).loanId(loan.getId()).minCap(minCap).maxCap(maxCap) - .installmentChargeData(loanInstallmentChargeDataSet).externalId(externalId).build(); + .installmentChargeData(loanInstallmentChargeDataList).externalId(externalId).build(); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java index 5da30a483cc..57783668d37 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java @@ -183,7 +183,7 @@ public Integer getCompoundingFrequencyOnDay() { } public boolean isCompoundingToBePostedAsTransaction() { - return null == this.isCompoundingToBePostedAsTransaction ? false : this.isCompoundingToBePostedAsTransaction; + return this.isCompoundingToBePostedAsTransaction != null && this.isCompoundingToBePostedAsTransaction; } public boolean allowCompoundingOnEod() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java index 643c114f1b4..c14074199ff 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java @@ -253,6 +253,15 @@ public static boolean isInPeriod(LocalDate targetDate, LocalDate fromDate, Local : DateUtils.isDateInRangeFromExclusiveToInclusive(targetDate, fromDate, toDate); } + public static boolean isBeforePeriod(LocalDate targetDate, LoanRepaymentScheduleInstallment installment, boolean isFirstPeriod) { + LocalDate fromDate = installment.getFromDate(); + return isFirstPeriod ? DateUtils.isBefore(targetDate, fromDate) : !DateUtils.isAfter(targetDate, fromDate); + } + + public static boolean isAfterPeriod(LocalDate targetDate, LoanRepaymentScheduleInstallment installment) { + return DateUtils.isAfter(targetDate, installment.getDueDate()); + } + public static Optional findInPeriod(LocalDate targetDate, List installments) { int firstNumber = fetchFirstNormalInstallmentNumber(installments); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index 07263eeb7ae..54216939690 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -101,6 +101,16 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat String FIND_ALL_LOAN_IDS_BY_STATUS_ID = "SELECT loan.id FROM Loan loan WHERE loan.loanStatus = :statusId"; + String FIND_LOANS_FOR_ACCRUAL = "select l from Loan l left join l.loanInterestRecalculationDetails recalcDetails " + + "where l.loanStatus = 300 and l.isNpa = false and l.chargedOff = false " + + "and l.loanProduct.accountingRule = :accountingType " + + "and (recalcDetails.isCompoundingToBePostedAsTransaction is null or recalcDetails.isCompoundingToBePostedAsTransaction = false) " + + "and exists (select ls.id from LoanRepaymentScheduleInstallment ls where ls.loan.id = l.id and ls.isDownPayment = false " + + "and ls.fromDate < :tillDate or (ls.installmentNumber = 1 and ls.fromDate = :tillDate) " + + "and ((ls.interestCharged is not null and ls.interestCharged <> ls.interestAccrued) " + + "or (ls.feeChargesCharged is not null and ls.feeChargesCharged <> ls.feeAccrued) " + + "or (ls.penaltyCharges is not null and ls.penaltyCharges <> ls.penaltyAccrued)))"; + @Query(FIND_GROUP_LOANS_DISBURSED_AFTER) List getGroupLoansDisbursedAfter(@Param("disbursementDate") LocalDate disbursementDate, @Param("groupId") Long groupId, @Param("loanType") Integer loanType); @@ -227,4 +237,7 @@ List findAllNonClosedLoansByLastClosedBusinessDateNotNullAndMinAndMaxLoanI @Query(FIND_ALL_LOAN_IDS_BY_STATUS_ID) List findLoanIdByStatusId(@Param("statusId") Integer statusId); + + @Query(FIND_LOANS_FOR_ACCRUAL) + List findLoansForAccrual(@Param("accountingType") Integer accountingType, @Param("tillDate") LocalDate tillDate); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java index 959d71eda26..84ac63fabea 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java @@ -263,4 +263,8 @@ public List findLoanIdsByStatusId(Integer statusId) { return repository.findLoanIdByStatusId(statusId); } + public List findLoansForAccrual(Integer accountingType, LocalDate tillDate) { + return repository.findLoansForAccrual(accountingType, tillDate); + } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 54ca17930bb..be77c17f552 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -252,26 +252,29 @@ public static LoanTransaction waiver(final Office office, final Loan loan, final public static LoanTransaction accrueInterest(final Office office, final Loan loan, final Money amount, final LocalDate interestAppliedDate, final ExternalId externalId) { - BigDecimal principalPortion = null; - BigDecimal feesPortion = null; - BigDecimal penaltiesPortion = null; BigDecimal interestPortion = amount.getAmount(); - BigDecimal overPaymentPortion = null; - boolean reversed = false; - PaymentDetail paymentDetail = null; - return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), interestAppliedDate, interestPortion, - principalPortion, interestPortion, feesPortion, penaltiesPortion, overPaymentPortion, reversed, paymentDetail, externalId); + return accrueTransaction(loan, office, interestAppliedDate, interestPortion, interestPortion, null, null, externalId); + } + + public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate, + final Money feeCharges, final Money penaltyCharges, final ExternalId externalId) { + final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate, + externalId); + applyCharge.updateChargesComponents(feeCharges, penaltyCharges); + return applyCharge; } public static LoanTransaction accrueTransaction(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount, final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion, final ExternalId externalId) { - BigDecimal principalPortion = null; - BigDecimal overPaymentPortion = null; - boolean reversed = false; - PaymentDetail paymentDetail = null; - return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf, amount, principalPortion, interestPortion, - feeChargesPortion, penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId); + return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf, amount, null, interestPortion, + feeChargesPortion, penaltyChargesPortion, null, false, null, externalId); + } + + public static LoanTransaction accrualAdjustment(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount, + final BigDecimal interestPortion, final BigDecimal feePortion, final BigDecimal penaltyPortion, final ExternalId externalId) { + return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue(), dateOf, amount, null, interestPortion, + feePortion, penaltyPortion, null, false, null, externalId); } public static LoanTransaction initiateTransfer(final Office office, final Loan loan, final LocalDate transferDate, @@ -325,14 +328,6 @@ public LoanTransaction copyTransactionPropertiesAndMappings() { return newTransaction; } - public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate, - final Money feeCharges, final Money penaltyCharges, final ExternalId externalId) { - final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate, - externalId); - applyCharge.updateChargesComponents(feeCharges, penaltyCharges); - return applyCharge; - } - public static LoanTransaction creditBalanceRefund(final Loan loan, final Office office, final Money amount, final LocalDate paymentDate, final ExternalId externalId, PaymentDetail paymentDetail) { return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND.getValue(), paymentDate, amount.getAmount(), @@ -720,10 +715,6 @@ public boolean isReAmortize() { return getTypeOf().isReAmortize() && isNotReversed(); } - public boolean isAccrualActivity() { - return getTypeOf().isAccrualActivity(); - } - public boolean isIdentifiedBy(final Long identifier) { return getId().equals(identifier); } @@ -871,16 +862,37 @@ public void updateExternalId(final ExternalId externalId) { } public boolean isAccrual() { - return LoanTransactionType.ACCRUAL.equals(getTypeOf()) && isNotReversed(); + return getTypeOf().isAccrual(); + } + + public boolean isAccrualAdjustment() { + return getTypeOf().isAccrualAdjustment(); + } + + public boolean isAccrualActivity() { + return getTypeOf().isAccrualActivity(); + } + + public boolean isAccrualRelated() { + return isAccrual() || isAccrualAdjustment() || isAccrualActivity(); + } + + public boolean isWaiveCharge() { + return getTypeOf().isWaiveCharges(); + } + + public boolean isWaiveInterest() { + return getTypeOf().isWaiveInterest(); } public boolean isNonMonetaryTransaction() { - return isNotReversed() && (LoanTransactionType.CONTRA.equals(getTypeOf()) - || LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf()) || LoanTransactionType.ACCRUAL.equals(getTypeOf()) - || LoanTransactionType.ACCRUAL_ACTIVITY.equals(getTypeOf()) || LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf()) - || LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf()) || LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) - || LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf()) || LoanTransactionType.CHARGE_OFF.equals(getTypeOf()) - || LoanTransactionType.REAMORTIZE.equals(getTypeOf()) || LoanTransactionType.REAGE.equals(getTypeOf())); + LoanTransactionType type = getTypeOf(); + return isNotReversed() && (type == LoanTransactionType.CONTRA || type == LoanTransactionType.MARKED_FOR_RESCHEDULING + || type == LoanTransactionType.ACCRUAL || type == LoanTransactionType.ACCRUAL_ACTIVITY + || type == LoanTransactionType.ACCRUAL_ADJUSTMENT || type == LoanTransactionType.APPROVE_TRANSFER + || type == LoanTransactionType.INITIATE_TRANSFER || type == LoanTransactionType.REJECT_TRANSFER + || type == LoanTransactionType.WITHDRAW_TRANSFER || type == LoanTransactionType.CHARGE_OFF + || type == LoanTransactionType.REAMORTIZE || type == LoanTransactionType.REAGE); } public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) { @@ -985,11 +997,7 @@ public Set getLoanTransactionToRepaym } public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo() { - return isDisbursement() || isAccrual() || isRepaymentAtDisbursement() || isRepayment() || isAccrualActivity(); - } - - public boolean isAccrualTransaction() { - return isAccrual(); + return isNotReversed() && (isDisbursement() || isAccrualRelated() || isRepaymentAtDisbursement() || isRepayment()); } public Money getOutstandingLoanBalanceMoney(final MonetaryCurrency currency) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java index c879ec62f83..84a08abf546 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionToRepaymentScheduleMapping.java @@ -30,6 +30,7 @@ import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanproduct.domain.AllocationType; @Entity @Table(name = "m_loan_transaction_repayment_schedule_mapping") @@ -146,6 +147,15 @@ public Money getPenaltyChargesPortion(final MonetaryCurrency currency) { return Money.of(currency, this.penaltyChargesPortion); } + public BigDecimal getPortion(AllocationType allocationType) { + return switch (allocationType) { + case PRINCIPAL -> getPrincipalPortion(); + case INTEREST -> getInterestPortion(); + case FEE -> getFeeChargesPortion(); + case PENALTY -> getPenaltyChargesPortion(); + }; + } + public LoanTransaction getLoanTransaction() { return loanTransaction; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java index 2af4b1d9076..967f24a2f75 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java @@ -66,6 +66,7 @@ public enum LoanTransactionType { INTEREST_PAYMENT_WAIVER(31, "loanTransactionType.interestPaymentWaiver"), // ACCRUAL_ACTIVITY(32, "loanTransactionType.accrualActivity"), // INTEREST_REFUND(33, "loanTransactionType.interestRefund"), // + ACCRUAL_ADJUSTMENT(34, "loanTransactionType.accrualAdjustment"), // ; private final Integer value; @@ -115,6 +116,7 @@ public static LoanTransactionType fromInt(final Integer transactionType) { case 31 -> LoanTransactionType.INTEREST_PAYMENT_WAIVER; case 32 -> LoanTransactionType.ACCRUAL_ACTIVITY; case 33 -> LoanTransactionType.INTEREST_REFUND; + case 34 -> LoanTransactionType.ACCRUAL_ADJUSTMENT; default -> LoanTransactionType.INVALID; }; } @@ -228,4 +230,7 @@ public boolean isInterestRefund() { return this.equals(LoanTransactionType.INTEREST_REFUND); } + public boolean isAccrualAdjustment() { + return this == LoanTransactionType.ACCRUAL_ADJUSTMENT; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java index a3237dd7e4e..ae0c48505ad 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapper.java @@ -45,14 +45,14 @@ public void reprocess(final MonetaryCurrency currency, final LocalDate disbursem totalInterest = totalInterest.plus(installment.getInterestCharged(currency)); totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); } - LoanChargePaidBy accrualBy = null; + List accruals = null; if (!loan.isInterestBearing() && loanCharge.isSpecifiedDueDate()) { // TODO: why only if not interest bearing LoanRepaymentScheduleInstallment addedPeriod = addChargeOnlyRepaymentInstallmentIfRequired(loanCharge, installments); if (addedPeriod != null) { addedPeriod.updateObligationsMet(currency, disbursementDate); } - accrualBy = loanCharge.getLoanChargePaidBySet().stream().filter(e -> e.getLoanTransaction().isAccrual()).findFirst() - .orElse(null); + accruals = loanCharge.getLoanChargePaidBySet().stream().filter(e -> !e.getLoanTransaction().isReversed() + && (e.getLoanTransaction().isAccrual() || e.getLoanTransaction().isAccrualAdjustment())).toList(); } LocalDate startDate = disbursementDate; int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); @@ -61,7 +61,8 @@ public void reprocess(final MonetaryCurrency currency, final LocalDate disbursem continue; } boolean installmentChargeApplicable = !installment.isRecalculatedInterestComponent(); - boolean isFirstPeriod = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber); + Integer installmentNumber = installment.getInstallmentNumber(); + boolean isFirstPeriod = installmentNumber.equals(firstNormalInstallmentNumber); Predicate feePredicate = e -> e.isFeeCharge() && !e.isDueAtDisbursement(); LocalDate dueDate = installment.getDueDate(); final Money feeChargesDue = calcChargeDue(startDate, dueDate, loanCharge, currency, installment, totalPrincipal, totalInterest, @@ -82,13 +83,19 @@ public void reprocess(final MonetaryCurrency currency, final LocalDate disbursem installment.addToChargePortion(feeChargesDue, feeChargesWaived, feeChargesWrittenOff, penaltyChargesDue, penaltyChargesWaived, penaltyChargesWrittenOff); - if (accrualBy != null && installment.isAdditional() && loanCharge.isDueInPeriod(startDate, dueDate, isFirstPeriod)) { - Money amount = Money.of(currency, accrualBy.getAmount()); + if (accruals != null && !accruals.isEmpty() && installment.isAdditional() + && loanCharge.isDueInPeriod(startDate, dueDate, isFirstPeriod)) { + BigDecimal amount = null; + for (LoanChargePaidBy accrual : accruals) { + accrual.setInstallmentNumber(installmentNumber); + amount = accrual.getLoanTransaction().isAccrual() ? MathUtil.add(amount, accrual.getAmount()) + : MathUtil.subtract(amount, accrual.getAmount()); + } + Money accruedAmount = Money.of(currency, MathUtil.negativeToZero(amount)); boolean isFee = loanCharge.isFeeCharge(); installment.updateAccrualPortion(installment.getInterestAccrued(currency), - MathUtil.plus(installment.getFeeAccrued(currency), (isFee ? amount : null)), - MathUtil.plus(installment.getPenaltyAccrued(currency), (isFee ? null : amount))); - accrualBy.setInstallmentNumber(installment.getInstallmentNumber()); + MathUtil.plus(installment.getFeeAccrued(currency), (isFee ? accruedAmount : null)), + MathUtil.plus(installment.getPenaltyAccrued(currency), (isFee ? null : accruedAmount))); } startDate = dueDate; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java index 916ebfef6c1..1b92029e92b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java @@ -18,6 +18,11 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isAfterPeriod; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isBeforePeriod; + +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; @@ -2805,4 +2810,26 @@ public OutstandingAmountsDTO calculatePrepaymentAmount(final MonetaryCurrency cu .feeCharges(feeCharges) // .penaltyCharges(penaltyCharges); } + + @Override + public Money getDueInterest(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate targetDate) { + if (isBeforePeriod(targetDate, installment, false)) { + return null; + } + MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency(); + if (isAfterPeriod(targetDate, installment) || DateUtils.isEqual(targetDate, installment.getDueDate())) { + return installment.getInterestCharged(currency); + } + + BigDecimal interestPortion; + LocalDate fromDate = installment.getFromDate(); + boolean isFirst = installment.getInstallmentNumber() + .equals(fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments())); + LocalDate startDate = isFirst ? fromDate.plusDays(1) : fromDate; + int totalNumberOfDays = DateUtils.getExactDifferenceInDays(startDate, installment.getDueDate()); + int daysToBeAccrued = DateUtils.getExactDifferenceInDays(startDate, targetDate); + double interestPerDay = installment.getInterestCharged().doubleValue() / totalNumberOfDays; + interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued); + return Money.of(currency, interestPortion); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java index 9cb841f5ffc..5506b7dbb78 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGenerator.java @@ -18,14 +18,17 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; +import jakarta.validation.constraints.NotNull; import java.math.MathContext; import java.time.LocalDate; import java.util.Set; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; @@ -42,4 +45,5 @@ OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, Local MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor); + Money getDueInterest(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate targetDate); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java index cf32cbf7635..873208819bd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java @@ -18,8 +18,9 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; -import java.util.Collection; +import java.util.List; import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -27,21 +28,22 @@ public interface LoanAccrualsProcessingService { - void addPeriodicAccruals(LocalDate tilldate) throws MultiException; + void addPeriodicAccruals(@NotNull LocalDate tilldate) throws MultiException; - void addPeriodicAccruals(LocalDate tilldate, Loan loan) throws MultiException; + void addPeriodicAccruals(@NotNull LocalDate tilldate, @NotNull Loan loan) throws MultiException; - void addAccrualAccounting(Long loanId, Collection loanScheduleAccrualDatas) throws Exception; + void addAccrualAccounting(@NotNull Long loanId, @NotNull List loanScheduleAccrualDatas) throws Exception; void addIncomeAndAccrualTransactions(Long loanId) throws Exception; - void reprocessExistingAccruals(Loan loan); + void reprocessExistingAccruals(@NotNull Loan loan); - void processAccrualsForInterestRecalculation(Loan loan, boolean isInterestRecalculationEnabled); + void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled); - void processIncomePostingAndAccruals(Loan loan); + void processIncomePostingAndAccruals(@NotNull Loan loan); - void processAccrualsForLoanClosure(Loan loan); + void processAccrualsForLoanClosure(@NotNull Loan loan); - void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDate, Collection newAccrualTransactions); + void processAccrualsForLoanForeClosure(@NotNull Loan loan, @NotNull LocalDate foreClosureDate, + @NotNull List newAccrualTransactions); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java new file mode 100644 index 00000000000..d8c12da8312 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java @@ -0,0 +1,50 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.util.Map; +import org.apache.fineract.infrastructure.codes.domain.CodeValue; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.portfolio.fund.domain.Fund; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.useradministration.domain.AppUser; + +public interface LoanAssembler { + + Loan assembleFrom(Long accountId); + + Loan assembleFrom(JsonCommand command); + + void setHelpers(Loan loanAccount); + + void accountNumberGeneration(JsonCommand command, Loan loan); + + CodeValue findCodeValueByIdIfProvided(Long codeValueId); + + Fund findFundByIdIfProvided(Long fundId); + + Staff findLoanOfficerByIdIfProvided(Long loanOfficerId); + + Map updateFrom(JsonCommand command, Loan loan); + + Map updateLoanApplicationAttributesForWithdrawal(Loan loan, JsonCommand command, AppUser currentUser); + + Map updateLoanApplicationAttributesForRejection(Loan loan, JsonCommand command, AppUser currentUser); +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java index fb2588fb441..b5d08e63b2a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformService.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.util.Collection; +import java.util.List; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.domain.Charge; @@ -38,11 +39,11 @@ public interface LoanChargeReadPlatformService { Collection retrieveLoanChargesForFeePayment(Integer paymentMode, Integer loanStatus); - Collection retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges); + List retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges); Collection retrieveOverdueInstallmentChargeFrequencyNumber(Loan loan, Charge charge, Integer periodNumber); - Collection retrieveLoanChargesForAccrual(Long loanId); + List retrieveLoanChargesForAccrual(Long loanId); Collection retrieveLoanChargesPaidBy(Long chargeId, LoanTransactionType transactionType, Integer installmentNumber); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java index 4db9c5cdcc2..acf6abb04c1 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java @@ -323,6 +323,8 @@ public static LoanTransactionEnumData transactionType(final LoanTransactionType LoanTransactionType.ACCRUAL_ACTIVITY.getCode(), "Accrual Activity"); case INTEREST_REFUND -> new LoanTransactionEnumData(LoanTransactionType.INTEREST_REFUND.getValue().longValue(), LoanTransactionType.INTEREST_REFUND.getCode(), "Interest Refund"); + case ACCRUAL_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue().longValue(), + LoanTransactionType.ACCRUAL_ADJUSTMENT.getCode(), "Accrual Adjustment"); }; } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index d2d97484c2f..956d10514aa 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -49,6 +49,7 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -72,6 +73,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; @@ -92,6 +94,9 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; +import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; @Slf4j @RequiredArgsConstructor @@ -101,6 +106,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME = "Advanced payment allocation strategy"; public final EMICalculator emiCalculator; + public final LoanRepositoryWrapper loanRepositoryWrapper; @Override public String getCode() { @@ -148,7 +154,7 @@ public Money handleRepaymentSchedule(List transactionsPostDisbu // only for progressive loans public Pair reprocessProgressiveLoanTransactions( - LocalDate disbursementDate, LocalDate currentDate, List loanTransactions, MonetaryCurrency currency, + LocalDate disbursementDate, LocalDate targetDate, List loanTransactions, MonetaryCurrency currency, List installments, Set charges) { final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); if (loanTransactions.isEmpty()) { @@ -175,8 +181,8 @@ public Pair repr final Loan loan = loanTransactions.get(0).getLoan(); final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf(); final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail(); - ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateModel(loanProductRelatedDetail, - installmentAmountInMultiplesOf, installments, overpaymentHolder.getMoneyObject().getMc()); + ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments, + loanProductRelatedDetail, installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc()); ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail, scheduleModel); @@ -212,7 +218,7 @@ public Pair repr LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId); createNewTransaction(oldTransaction, newTransaction, ctx); } - recalculateInterestForDate(currentDate, ctx); + recalculateInterestForDate(targetDate, ctx); List txs = changeOperations.stream() // .filter(ChangeOperation::isTransaction) // .map(e -> e.getLoanTransaction().get()).toList(); @@ -220,6 +226,32 @@ public Pair repr return Pair.of(changedTransactionDetail, scheduleModel); } + @Override + public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, + MonetaryCurrency currency, List installments, Set charges) { + LocalDate currentDate = DateUtils.getBusinessLocalDate(); + return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges) + .getLeft(); + } + + @NotNull + @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) + public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(@NotNull Long loanId, LocalDate targetDate) { + Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); + List transactions = loan.retrieveListOfTransactionsForReprocessing(); + MonetaryCurrency currency = loan.getLoanRepaymentScheduleDetail().getCurrency(); + List installments = loan.getRepaymentScheduleInstallments(); + Set charges = loan.getActiveCharges(); + return reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), targetDate, transactions, currency, installments, charges) + .getRight(); + } + + @NotNull + private static LoanTransaction getProcessedTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction transaction) { + LoanTransaction newTransaction = changedTransactionDetail.getNewTransactionMappings().get(transaction.getId()); + return newTransaction == null ? transaction : newTransaction; + } + private void processInterestRateChange(final List installments, final LoanTermVariationsData interestRateChange, final ProgressiveLoanInterestScheduleModel scheduleModel) { final LocalDate interestRateChangeSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom(); @@ -244,29 +276,14 @@ private void updateInstallmentIfInterestPeriodPresent(final ProgressiveLoanInter }); } - @Override - public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, - MonetaryCurrency currency, List installments, Set charges) { - LocalDate currentDate = DateUtils.getBusinessLocalDate(); - return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges) - .getLeft(); - } - - @NotNull - private static LoanTransaction getProcessedTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction transaction) { - LoanTransaction newTransaction = changedTransactionDetail.getNewTransactionMappings().get(transaction.getId()); - return newTransaction == null ? transaction : newTransaction; - } - @Override public void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) { switch (loanTransaction.getTypeOf()) { case DISBURSEMENT -> handleDisbursement(loanTransaction, ctx); - case WRITEOFF -> handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments()); - case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges()); + case WRITEOFF -> handleWriteOff(loanTransaction, ctx); + case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx); case CHARGEBACK -> handleChargeback(loanTransaction, ctx); - case CREDIT_BALANCE_REFUND -> - handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder()); + case CREDIT_BALANCE_REFUND -> handleCreditBalanceRefund(loanTransaction, ctx); case INTEREST_REFUND, REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT, WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER -> handleRepayment(loanTransaction, ctx); @@ -275,7 +292,7 @@ public void processLatestTransaction(LoanTransaction loanTransaction, Transactio case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed."); case REAMORTIZE -> handleReAmortization(loanTransaction, ctx); case REAGE -> handleReAge(loanTransaction, ctx); - case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx.getCurrency(), ctx.getInstallments()); + case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx); // TODO: Cover rest of the transaction types default -> { log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf()); @@ -332,9 +349,14 @@ protected void handleChargeback(LoanTransaction loanTransaction, TransactionCtx processCreditTransaction(loanTransaction, ctx); } + protected void handleCreditBalanceRefund(LoanTransaction transaction, TransactionCtx ctx) { + super.handleCreditBalanceRefund(transaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder()); + } + private boolean hasNoCustomCreditAllocationRule(LoanTransaction loanTransaction) { - return (loanTransaction.getLoan().getCreditAllocationRules() == null || !loanTransaction.getLoan().getCreditAllocationRules() - .stream().anyMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf()))); + List creditAllocationRules = loanTransaction.getLoan().getCreditAllocationRules(); + return (creditAllocationRules == null || creditAllocationRules.stream() + .noneMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf()))); } protected LoanTransaction findChargebackOriginalTransaction(LoanTransaction chargebackTransaction, TransactionCtx ctx) { @@ -370,79 +392,79 @@ protected void processCreditTransaction(LoanTransaction loanTransaction, Transac if (hasNoCustomCreditAllocationRule(loanTransaction)) { super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments()); } else { - loanTransaction.resetDerivedComponents(); + MonetaryCurrency currency = ctx.getCurrency(); final Comparator byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate); ctx.getInstallments().sort(byDate); - final Money zeroMoney = Money.zero(ctx.getCurrency()); - Money transactionAmount = loanTransaction.getAmount(ctx.getCurrency()); + final Money zeroMoney = Money.zero(currency); + Money transactionAmount = loanTransaction.getAmount(currency); Money totalOverpaid = ctx.getOverpaymentHolder().getMoneyObject(); - Money amountToDistribute = MathUtil.negativeToZero(loanTransaction.getAmount(ctx.getCurrency()).minus(totalOverpaid)); + Money amountToDistribute = MathUtil.negativeToZero(transactionAmount).minus(totalOverpaid); Money overpaymentAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute)); + + loanTransaction.resetDerivedComponents(); loanTransaction.setOverPayments(overpaymentAmount); + if (!transactionAmount.isGreaterThanZero()) { + return; + } + if (!loanTransaction.isChargeback()) { + throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name()); + } - if (transactionAmount.isGreaterThanZero()) { - if (loanTransaction.isChargeback()) { - LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx); - // get the original allocation from the opriginal transaction - Map originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction, - ctx.getCurrency()); - LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction); - - // if there were earlier chargebacks then let's calculate the remaining amounts for each portion - Map originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction, - originalAllocationNotAdjusted, loanTransaction, ctx, chargeBackAllocationRule); - - // calculate the current chargeback allocation - Map chargebackAllocation = calculateChargebackAllocationMap(originalAllocation, - transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency()); - - loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST), - chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY)); - - final LocalDate transactionDate = loanTransaction.getTransactionDate(); - boolean loanTransactionMapped = false; - LocalDate pastDueDate = null; - for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) { - pastDueDate = currentInstallment.getDueDate(); - if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) { - recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation); - loanTransactionMapped = true; - break; - - // If already exists an additional installment just update the due date and - // principal from the Loan chargeback / CBR transaction - } else if (currentInstallment.isAdditional()) { - if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) { - currentInstallment.updateDueDate(transactionDate); - } - recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation); - loanTransactionMapped = true; - break; - } - } + LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx); + // get the original allocation from the opriginal transaction + Map originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction, currency); + LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction); + + // if there were earlier chargebacks then let's calculate the remaining amounts for each portion + Map originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction, + originalAllocationNotAdjusted, loanTransaction, ctx, chargeBackAllocationRule); + + // calculate the current chargeback allocation + Map chargebackAllocation = calculateChargebackAllocationMap(originalAllocation, + transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), currency); + + loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST), + chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY)); + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + boolean loanTransactionMapped = false; + LocalDate pastDueDate = null; + for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) { + pastDueDate = currentInstallment.getDueDate(); + if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) { + recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation); + loanTransactionMapped = true; + break; - // New installment will be added (N+1 scenario) - if (!loanTransactionMapped) { - if (loanTransaction.getTransactionDate().equals(pastDueDate)) { - LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments() - .get(ctx.getInstallments().size() - 1); - recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation); - } else { - Loan loan = loanTransaction.getLoan(); - LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, - (ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(), - zeroMoney.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), false, null); - recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, installment, chargebackAllocation); - installment.markAsAdditional(); - loan.addLoanRepaymentScheduleInstallment(installment); - } + // If already exists an additional installment just update the due date and + // principal from the Loan chargeback / CBR transaction + } else if (currentInstallment.isAdditional()) { + if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) { + currentInstallment.updateDueDate(transactionDate); } - allocateOverpayment(loanTransaction, ctx); + recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation); + loanTransactionMapped = true; + break; + } + } + + // New installment will be added (N+1 scenario) + if (!loanTransactionMapped) { + if (loanTransaction.getTransactionDate().equals(pastDueDate)) { + LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments().get(ctx.getInstallments().size() - 1); + recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation); } else { - throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name()); + Loan loan = loanTransaction.getLoan(); + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, + (ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(), zeroMoney.getAmount(), + zeroMoney.getAmount(), zeroMoney.getAmount(), false, null); + recognizeAmountsAfterChargeback(ctx, transactionDate, installment, chargebackAllocation); + installment.markAsAdditional(); + loan.addLoanRepaymentScheduleInstallment(installment); } } } + allocateOverpayment(loanTransaction, ctx); } private Map adjustOriginalAllocationWithFormerChargebacks(LoanTransaction originalTransaction, @@ -499,14 +521,15 @@ private Comparator loanTransactionDateComparator() { }; } - private void recognizeAmountsAfterChargeback(MonetaryCurrency currency, LocalDate localDate, + private void recognizeAmountsAfterChargeback(TransactionCtx ctx, LocalDate transactionDate, LoanRepaymentScheduleInstallment installment, Map chargebackAllocation) { Money principal = chargebackAllocation.get(PRINCIPAL); if (principal.isGreaterThanZero()) { installment.addToCreditedPrincipal(principal.getAmount()); - installment.addToPrincipal(localDate, principal); + installment.addToPrincipal(transactionDate, principal); } + MonetaryCurrency currency = ctx.getCurrency(); Money fee = chargebackAllocation.get(FEE); if (fee.isGreaterThanZero()) { installment.addToCreditedFee(fee.getAmount()); @@ -565,9 +588,8 @@ private Predicate hasMatchingToLoanTransaction(Long id, return relation -> relation.getRelationType().equals(typeEnum) && Objects.equals(relation.getToTransaction().getId(), id); } - @Override - protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Set charges) { + protected void handleRefund(LoanTransaction loanTransaction, TransactionCtx ctx) { + MonetaryCurrency currency = ctx.getCurrency(); Money zero = Money.zero(currency); List transactionMappings = new ArrayList<>(); Money transactionAmountUnprocessed = loanTransaction.getAmount(currency); @@ -592,18 +614,16 @@ protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency cu Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList()))); for (Map.Entry> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) { - transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, currency, installments, - transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule, - transactionMappings, charges, balances); + transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, ctx, transactionAmountUnprocessed, + paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule, transactionMappings, balances); if (!transactionAmountUnprocessed.isGreaterThanZero()) { break; } } } else if (scheduleProcessingType.isVertical()) { for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { - transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, currency, installments, zero, - transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, charges, balances, - paymentAllocationType); + transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, ctx, transactionMappings, + transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, paymentAllocationType); if (!transactionAmountUnprocessed.isGreaterThanZero()) { break; } @@ -798,97 +818,99 @@ private List createSortedChangeList(final LoanTermVariationsDat return changeOperations; } + private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { + // TODO: Fix this and enhance EMICalculator to support reamortization and reaging + if (disbursementTransaction.getLoan().isInterestBearing()) { + handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx); + } else { + handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx); + } + } + private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { - ProgressiveTransactionCtx progressiveTransactionCtx = (ProgressiveTransactionCtx) transactionCtx; - if (progressiveTransactionCtx.getModel() == null) { + ProgressiveLoanInterestScheduleModel model; + if (!(transactionCtx instanceof ProgressiveTransactionCtx) + || (model = ((ProgressiveTransactionCtx) transactionCtx).getModel()) == null) { throw new IllegalStateException("TransactionCtx has no model"); } - disbursementTransaction.resetDerivedComponents(); final MathContext mc = MoneyHelper.getMathContext(); - LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail(); - Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf(); - Money downPaymentAmount = Money.zero(progressiveTransactionCtx.getCurrency()); + Loan loan = disbursementTransaction.getLoan(); + LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail(); + Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf(); + List installments = transactionCtx.getInstallments(); + LocalDate transactionDate = disbursementTransaction.getTransactionDate(); + MonetaryCurrency currency = transactionCtx.getCurrency(); + Money downPaymentAmount = Money.zero(currency); if (loanProductRelatedDetail.isEnableDownPayment()) { - LoanRepaymentScheduleInstallment downPaymentInstallment = progressiveTransactionCtx.getInstallments().stream() - .filter(i -> i.isDownPayment() && i.getPrincipal(progressiveTransactionCtx.getCurrency()).isZero()).findFirst() - .orElseThrow(); BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(), loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc); if (installmentAmountInMultiplesOf != null) { downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf); } - downPaymentAmount = Money.of(transactionCtx.getCurrency(), downPaymentAmt); - downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount); + downPaymentAmount = Money.of(currency, downPaymentAmt); + LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream() + .filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow(); + downPaymentInstallment.addToPrincipal(transactionDate, downPaymentAmount); } - Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount); - - emiCalculator.addDisbursement(progressiveTransactionCtx.getModel(), disbursementTransaction.getTransactionDate(), - amortizableAmount); + Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount); + emiCalculator.addDisbursement(model, transactionDate, amortizableAmount); + disbursementTransaction.resetDerivedComponents(); if (amortizableAmount.isGreaterThanZero()) { - progressiveTransactionCtx.getModel().repaymentPeriods().forEach(rm -> { - LoanRepaymentScheduleInstallment installment = transactionCtx.getInstallments().stream() - .filter(ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() - && !ri.getDueDate().isBefore(disbursementTransaction.getTransactionDate())) + model.repaymentPeriods().forEach(rm -> { + LoanRepaymentScheduleInstallment installment = installments.stream().filter( + ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate)) .findFirst().orElse(null); if (installment != null) { installment.updatePrincipal(rm.getDuePrincipal().getAmount()); installment.updateInterestCharged(rm.getDueInterest().getAmount()); - installment.updateObligationsMet(progressiveTransactionCtx.getCurrency(), disbursementTransaction.getTransactionDate()); + installment.updateObligationsMet(currency, transactionDate); } }); } - allocateOverpayment(disbursementTransaction, transactionCtx); } - private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { - // TODO: Fix this and enhance EMICalculator to support reamortization and reaging - if (disbursementTransaction.getLoan().isInterestBearing()) { - handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx); - } else { - handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx); - } - } - private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { disbursementTransaction.resetDerivedComponents(); final MathContext mc = MoneyHelper.getMathContext(); - List candidateRepaymentInstallments = transactionCtx.getInstallments().stream().filter( + MonetaryCurrency currency = transactionCtx.getCurrency(); + List installments = transactionCtx.getInstallments(); + List candidateRepaymentInstallments = installments.stream().filter( i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional()) .toList(); int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size(); LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail(); Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf(); - Money downPaymentAmount = Money.zero(transactionCtx.getCurrency()); + Money downPaymentAmount = Money.zero(currency); if (loanProductRelatedDetail.isEnableDownPayment()) { - LoanRepaymentScheduleInstallment downPaymentInstallment = transactionCtx.getInstallments().stream() - .filter(i -> i.isDownPayment() && i.getPrincipal(transactionCtx.getCurrency()).isZero()).findFirst().orElseThrow(); + LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream() + .filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow(); BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(), loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc); if (installmentAmountInMultiplesOf != null) { downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf); } - downPaymentAmount = Money.of(transactionCtx.getCurrency(), downPaymentAmt); + downPaymentAmount = Money.of(currency, downPaymentAmt); downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount); } - Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount); + Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount); if (amortizableAmount.isGreaterThanZero()) { Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext()); MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount); candidateRepaymentInstallments.forEach(i -> { - Money previousPrincipal = i.getPrincipal(transactionCtx.getCurrency()); + Money previousPrincipal = i.getPrincipal(currency); Money newPrincipal = previousPrincipal.add(increasePrincipalBy); if (installmentAmountInMultiplesOf != null) { newPrincipal = Money.roundToMultiplesOf(newPrincipal, installmentAmountInMultiplesOf); } i.updatePrincipal(newPrincipal.getAmount()); moneyHolder.setMoneyObject(moneyHolder.getMoneyObject().minus(newPrincipal).plus(previousPrincipal)); - i.updateObligationsMet(transactionCtx.getCurrency(), disbursementTransaction.getTransactionDate()); + i.updateObligationsMet(currency, disbursementTransaction.getTransactionDate()); }); // Hence the rounding, we might need to amend the last installment amount candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1) @@ -918,52 +940,56 @@ private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx } } - private List findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate currentDate, + protected void handleWriteOff(final LoanTransaction transaction, TransactionCtx ctx) { + super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments()); + } + + private List findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate targetDate, ProgressiveTransactionCtx transactionCtx) { return transactionCtx.getInstallments().stream() // .filter(installment -> !installment.isDownPayment() && !installment.isAdditional()) - .filter(installment -> installment.isOverdueOn(currentDate)) + .filter(installment -> installment.isOverdueOn(targetDate)) .sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList(); } - private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransactionCtx ctx) { - if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty() - && ctx.getInstallments().get(0).getLoan().isInterestRecalculationEnabledForProduct() - && !ctx.getInstallments().get(0).getLoan().isNpa() && !ctx.getInstallments().get(0).getLoan().isChargedOff()) { - List overdueInstallmentsSortedByInstallmentNumber = findOverdueInstallmentsBeforeDateSortedByInstallmentNumber( - currentDate, ctx); - if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) { - List normalInstallments = ctx.getInstallments().stream() // - .filter(installment -> !installment.isAdditional() && !installment.isDownPayment()).toList(); - - Optional currentInstallmentOptional = normalInstallments.stream().filter( - installment -> installment.getFromDate().isBefore(currentDate) && !installment.getDueDate().isBefore(currentDate)) - .findAny(); - - // get DUE installment or last installment - LoanRepaymentScheduleInstallment lastInstallment = normalInstallments.stream() - .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).get(); - LoanRepaymentScheduleInstallment currentInstallment = currentInstallmentOptional.orElse(lastInstallment); - - Money overDuePrincipal = Money.zero(ctx.getCurrency()); - Money aggregatedOverDuePrincipal = Money.zero(ctx.getCurrency()); - for (LoanRepaymentScheduleInstallment processingInstallment : overdueInstallmentsSortedByInstallmentNumber) { - // add and subtract outstanding principal - if (!overDuePrincipal.isZero()) { - adjustOverduePrincipalForInstallment(currentDate, processingInstallment, overDuePrincipal, - aggregatedOverDuePrincipal, ctx); - } - - overDuePrincipal = processingInstallment.getPrincipalOutstanding(ctx.getCurrency()); - aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal); - } - - boolean adjustNeeded = !currentInstallment.equals(lastInstallment) || !lastInstallment.isOverdueOn(currentDate); - if (adjustNeeded) { - adjustOverduePrincipalForInstallment(currentDate, currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal, - ctx); + private void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) { + List installments = ctx.getInstallments(); + if (installments == null || installments.isEmpty()) { + return; + } + Loan loan = installments.get(0).getLoan(); + if (!loan.isInterestRecalculationEnabledForProduct() || loan.isNpa() || loan.isChargedOff()) { + return; + } + List overdueInstallmentsSortedByInstallmentNumber = findOverdueInstallmentsBeforeDateSortedByInstallmentNumber( + targetDate, ctx); + if (overdueInstallmentsSortedByInstallmentNumber.isEmpty()) { + return; + } + MonetaryCurrency currency = ctx.getCurrency(); + Money overDuePrincipal = Money.zero(currency); + Money aggregatedOverDuePrincipal = Money.zero(currency); + for (LoanRepaymentScheduleInstallment processingInstallment : overdueInstallmentsSortedByInstallmentNumber) { + // add and subtract outstanding principal + if (!overDuePrincipal.isZero()) { + adjustOverduePrincipalForInstallment(targetDate, processingInstallment, overDuePrincipal, aggregatedOverDuePrincipal, ctx); + } - } + overDuePrincipal = processingInstallment.getPrincipalOutstanding(currency); + aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal); + } + + List normalInstallments = installments.stream() // + .filter(installment -> !installment.isAdditional() && !installment.isDownPayment()).toList(); + LoanRepaymentScheduleInstallment lastInstallment = normalInstallments.stream() + .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).get(); + if (!lastInstallment.isOverdueOn(targetDate)) { + // get DUE installment or last installment + LoanRepaymentScheduleInstallment currentInstallment = LoanRepaymentScheduleProcessingWrapper + .findInPeriod(targetDate, normalInstallments).orElse(lastInstallment); + boolean adjustNeeded = !currentInstallment.equals(lastInstallment); + if (adjustNeeded) { + adjustOverduePrincipalForInstallment(targetDate, currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal, ctx); } } } @@ -974,7 +1000,9 @@ private void adjustOverduePrincipalForInstallment(LocalDate currentDate, LoanRep LocalDate toDate = currentInstallment.getDueDate(); boolean hasUpdate = false; - if (currentInstallment.getLoan().getLoanInterestRecalculationDetails().getRestFrequencyType().isSameAsRepayment()) { + RecalculationFrequencyType restFrequencyType = currentInstallment.getLoan().getLoanInterestRecalculationDetails() + .getRestFrequencyType(); + if (restFrequencyType.isSameAsRepayment()) { // if we have same date for fromDate & last overdue balance change then it means we have the up-to-date // model. if (ctx.getLastOverdueBalanceChange() == null || fromDate.isAfter(ctx.getLastOverdueBalanceChange())) { @@ -990,7 +1018,7 @@ private void adjustOverduePrincipalForInstallment(LocalDate currentDate, LoanRep } } - if (currentInstallment.getLoan().getLoanInterestRecalculationDetails().getRestFrequencyType().isDaily() + if (restFrequencyType.isDaily() // if we have same date for currentDate & last overdue balance change then it meas we have the // up-to-date model. && !currentDate.equals(ctx.getLastOverdueBalanceChange())) { @@ -1062,15 +1090,15 @@ private Money processPaymentAllocation(PaymentAllocationType paymentAllocationTy LoanTransaction loanTransaction, Money transactionAmountUnprocessed, LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Set chargesOfInstallment, Balances balances, LoanRepaymentScheduleInstallment.PaymentAction action) { + AllocationType allocationType = paymentAllocationType.getAllocationType(); + MonetaryCurrency currency = loanTransaction.getLoan().loanCurrency(); + Money zero = Money.zero(currency); LocalDate transactionDate = loanTransaction.getTransactionDate(); - Money zero = transactionAmountUnprocessed.zero(); - - LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment - .getPaymentFunction(paymentAllocationType.getAllocationType(), action); + LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment.getPaymentFunction(allocationType, action); ChargesPaidByFunction chargesPaidByFunction = getChargesPaymentFunction(action); Money portion = paymentFunction.accept(transactionDate, transactionAmountUnprocessed); - switch (paymentAllocationType.getAllocationType()) { + switch (allocationType) { case PENALTY -> { balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(portion)); addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, portion); @@ -1093,7 +1121,7 @@ private Money processPaymentAllocation(PaymentAllocationType paymentAllocationTy } } - currentInstallment.checkIfRepaymentPeriodObligationsAreMet(transactionDate, loanTransaction.getLoan().loanCurrency()); + currentInstallment.checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency); return portion; } @@ -1194,11 +1222,13 @@ private void handleChargePayment(LoanTransaction loanTransaction, TransactionCtx } } - private Money refundTransactionHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, + private Money refundTransactionHorizontally(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed, List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, - List transactionMappings, Set charges, Balances balances) { + List transactionMappings, Balances balances) { + MonetaryCurrency currency = ctx.getCurrency(); Money zero = Money.zero(currency); + List installments = ctx.getInstallments(); + Set charges = ctx.getCharges(); Money refundedPortion; outerLoop: do { LoanRepaymentScheduleInstallment latestPastDueInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency, @@ -1268,13 +1298,16 @@ private Money refundTransactionHorizontally(LoanTransaction loanTransaction, Mon return transactionAmountUnprocessed; } - private Money refundTransactionVertically(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money zero, + private Money refundTransactionVertically(LoanTransaction loanTransaction, TransactionCtx ctx, List transactionMappings, Money transactionAmountUnprocessed, - FutureInstallmentAllocationRule futureInstallmentAllocationRule, Set charges, Balances balances, + FutureInstallmentAllocationRule futureInstallmentAllocationRule, Balances balances, PaymentAllocationType paymentAllocationType) { - LoanRepaymentScheduleInstallment currentInstallment = null; + MonetaryCurrency currency = ctx.getCurrency(); + Money zero = Money.zero(currency); Money refundedPortion = zero; + List installments = ctx.getInstallments(); + Set charges = ctx.getCharges(); + LoanRepaymentScheduleInstallment currentInstallment = null; int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); do { switch (paymentAllocationType.getDueType()) { @@ -1397,7 +1430,6 @@ private Money processPeriods(LoanTransaction transaction, Money processAmount, S private Money processPeriods(LoanTransaction transaction, Money processAmount, LoanPaymentAllocationRule allocationRule, Set charges, List transactionMappings, Balances balances, TransactionCtx transactionCtx) { - MonetaryCurrency currency = transactionCtx.getCurrency(); LoanScheduleProcessingType scheduleProcessingType = transaction.getLoan().getLoanProductRelatedDetail() .getLoanScheduleProcessingType(); if (scheduleProcessingType.isHorizontal()) { @@ -1405,8 +1437,8 @@ private Money processPeriods(LoanTransaction transaction, Money processAmount, L balances); } if (scheduleProcessingType.isVertical()) { - return processPeriodsVertically(transaction, currency, transactionCtx.getInstallments(), processAmount, allocationRule, - transactionMappings, charges, balances); + return processPeriodsVertically(transaction, transactionCtx, processAmount, allocationRule, transactionMappings, charges, + balances); } return processAmount; } @@ -1434,10 +1466,7 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Tr return transactionAmountUnprocessed; } - MonetaryCurrency currency = transactionCtx.getCurrency(); List installments = transactionCtx.getInstallments(); - Money paidPortion; - boolean exit = false; do { LoanRepaymentScheduleInstallment oldestPastDueInstallment = installments.stream() .filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isAfter(e.getDueDate())) @@ -1449,66 +1478,34 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Tr // For having similar logic we are populating installment list even when the future installment // allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element. List inAdvanceInstallments = new ArrayList<>(); - if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) { - inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) - .filter(e -> loanTransaction.isBefore(e.getDueDate())).toList(); - } else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) { - inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) - .filter(e -> loanTransaction.isBefore(e.getDueDate())) + Stream inAdvanceInstallmentsStream = installments.stream() + .filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isBefore(e.getDueDate())); + if (futureInstallmentAllocationRule == FutureInstallmentAllocationRule.REAMORTIZATION) { + inAdvanceInstallments = inAdvanceInstallmentsStream.toList(); + } else if (futureInstallmentAllocationRule == FutureInstallmentAllocationRule.NEXT_INSTALLMENT) { + inAdvanceInstallments = inAdvanceInstallmentsStream .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); - } else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) { - inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) - .filter(e -> loanTransaction.isBefore(e.getDueDate())) + } else if (futureInstallmentAllocationRule == FutureInstallmentAllocationRule.LAST_INSTALLMENT) { + inAdvanceInstallments = inAdvanceInstallmentsStream .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); } int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); + Loan loan = loanTransaction.getLoan(); + boolean interestRecalc = transactionCtx instanceof ProgressiveTransactionCtx && loan.isInterestBearing() + && loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled(); for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { switch (paymentAllocationType.getDueType()) { case PAST_DUE -> { - if (oldestPastDueInstallment != null) { - Set oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, oldestPastDueInstallment, - firstNormalInstallmentNumber); - LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( - transactionMappings, loanTransaction, oldestPastDueInstallment, currency); - Loan loan = loanTransaction.getLoan(); - if (transactionCtx instanceof ProgressiveTransactionCtx ctx && loan.isInterestBearing() - && loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { - paidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction, - transactionAmountUnprocessed, balances, paymentAllocationType, oldestPastDueInstallment, ctx, - loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges); - } else { - paidPortion = processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, loanTransaction, - transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, - oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY); - } - transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); - } else { - exit = true; - } + transactionAmountUnprocessed = handleTransactionPaymentAllocation(loanTransaction, transactionCtx, + transactionAmountUnprocessed, transactionAmountUnprocessed, transactionMappings, charges, balances, + firstNormalInstallmentNumber, paymentAllocationType, oldestPastDueInstallment); } case DUE -> { - if (dueInstallment != null) { - Set dueInstallmentCharges = getLoanChargesOfInstallment(charges, dueInstallment, - firstNormalInstallmentNumber); - LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( - transactionMappings, loanTransaction, dueInstallment, currency); - Loan loan = loanTransaction.getLoan(); - if (transactionCtx instanceof ProgressiveTransactionCtx ctx && loan.isInterestBearing() - && loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { - paidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction, - transactionAmountUnprocessed, balances, paymentAllocationType, dueInstallment, ctx, - loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges); - } else { - paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction, - transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, - balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY); - } - transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); - } else { - exit = true; - } + transactionAmountUnprocessed = handleTransactionPaymentAllocation(loanTransaction, transactionCtx, + transactionAmountUnprocessed, transactionAmountUnprocessed, transactionMappings, charges, balances, + firstNormalInstallmentNumber, paymentAllocationType, dueInstallment); } case IN_ADVANCE -> { int numberOfInstallments = inAdvanceInstallments.size(); @@ -1519,31 +1516,16 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Tr // Adjustment might be needed due to the divide operation and the rounding mode Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) { - Set inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment, - firstNormalInstallmentNumber); - - LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( - transactionMappings, loanTransaction, inAdvanceInstallment, currency); - - Loan loan = loanTransaction.getLoan(); - if (transactionCtx instanceof ProgressiveTransactionCtx ctx && loan.isInterestBearing() - && loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { - paidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction, evenPortion, - balances, paymentAllocationType, inAdvanceInstallment, ctx, - loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges); - } else { - // Adjust the portion for the last installment - if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) { - evenPortion = evenPortion.add(balanceAdjustment); - } - paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, - evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances, - LoanRepaymentScheduleInstallment.PaymentAction.PAY); - } - transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); + Money processAmount = (!interestRecalc + && inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) + ? MathUtil.plus(evenPortion, balanceAdjustment) + : evenPortion; + transactionAmountUnprocessed = handleTransactionPaymentAllocation(loanTransaction, transactionCtx, + processAmount, transactionAmountUnprocessed, transactionMappings, charges, balances, + firstNormalInstallmentNumber, paymentAllocationType, inAdvanceInstallment); } } else { - exit = true; + transactionAmountUnprocessed = null; } } } @@ -1551,11 +1533,40 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Tr } // We are allocating till there is no pending installment or there is no more unprocessed transaction amount // or there is no more outstanding balance of the allocation type - while (!exit && installments.stream().anyMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) - && transactionAmountUnprocessed.isGreaterThanZero()); + while (!MathUtil.isEmpty(transactionAmountUnprocessed) + && installments.stream().anyMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)); return transactionAmountUnprocessed; } + private Money handleTransactionPaymentAllocation(@NotNull LoanTransaction loanTransaction, @NotNull TransactionCtx ctx, + @NotNull Money processAmount, @NotNull Money totalProcessAmount, + @NotNull List transactionMappings, Set charges, + @NotNull Balances balances, int firstNormalInstallmentNumber, @NotNull PaymentAllocationType paymentAllocationType, + LoanRepaymentScheduleInstallment installment) { + if (installment == null) { + return null; + } + MonetaryCurrency currency = ctx.getCurrency(); + Loan loan = loanTransaction.getLoan(); + boolean interestRecalc = ctx instanceof ProgressiveTransactionCtx && loan.isInterestBearing() + && loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled(); + Set installmentCharges = getLoanChargesOfInstallment(charges, installment, firstNormalInstallmentNumber); + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(transactionMappings, + loanTransaction, installment, currency); + Money paidPortion; + if (interestRecalc) { + paidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction, processAmount, balances, + paymentAllocationType, installment, (ProgressiveTransactionCtx) ctx, loanTransactionToRepaymentScheduleMapping, + installmentCharges); + } else { + // TODO emiCalculator.register paid amount + paidPortion = processPaymentAllocation(paymentAllocationType, installment, loanTransaction, processAmount, + loanTransactionToRepaymentScheduleMapping, installmentCharges, balances, + LoanRepaymentScheduleInstallment.PaymentAction.PAY); + } + return MathUtil.minusToZero(totalProcessAmount, paidPortion); + } + private Money handlingPaymentAllocationForInterestBearingProgressiveLoan(LoanTransaction loanTransaction, Money transactionAmountUnprocessed, Balances balances, PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment installment, ProgressiveTransactionCtx ctx, @@ -1573,7 +1584,7 @@ private Money handlingPaymentAllocationForInterestBearingProgressiveLoan(LoanTra if (DueType.IN_ADVANCE.equals(paymentAllocationType.getDueType())) { payDate = calculateNewPayDateInCaseOfInAdvancePayment(loanTransaction, installment); - updateRepaymentPeriodBalances(paymentAllocationType, installment, model, payDate); + updateRepaymentPeriodBalances(paymentAllocationType, installment, ctx, payDate); } paidPortion = processPaymentAllocation(paymentAllocationType, installment, loanTransaction, transactionAmountUnprocessed, @@ -1581,17 +1592,16 @@ private Money handlingPaymentAllocationForInterestBearingProgressiveLoan(LoanTra if (PRINCIPAL.equals(paymentAllocationType.getAllocationType())) { emiCalculator.payPrincipal(model, installment.getDueDate(), payDate, paidPortion); - updateRepaymentPeriods(loanTransaction, ctx, model); + updateRepaymentPeriods(loanTransaction, ctx); } else if (INTEREST.equals(paymentAllocationType.getAllocationType())) { emiCalculator.payInterest(model, installment.getDueDate(), payDate, paidPortion); - updateRepaymentPeriods(loanTransaction, ctx, model); + updateRepaymentPeriods(loanTransaction, ctx); } return paidPortion; } - private void updateRepaymentPeriods(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx, - ProgressiveLoanInterestScheduleModel model) { - model.repaymentPeriods().forEach(rm -> { + private void updateRepaymentPeriods(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx) { + ctx.getModel().repaymentPeriods().forEach(rm -> { LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream() .filter(ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment()).findFirst().orElse(null); if (installment != null) { @@ -1603,8 +1613,8 @@ private void updateRepaymentPeriods(LoanTransaction loanTransaction, Progressive } private void updateRepaymentPeriodBalances(PaymentAllocationType paymentAllocationType, - LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveLoanInterestScheduleModel model, LocalDate payDate) { - PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(model, inAdvanceInstallment.getDueDate(), payDate); + LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveTransactionCtx ctx, LocalDate payDate) { + PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(ctx.getModel(), inAdvanceInstallment.getDueDate(), payDate); switch (paymentAllocationType) { case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getDueInterest().getAmount()); @@ -1621,9 +1631,8 @@ private LocalDate calculateNewPayDateInCaseOfInAdvancePayment(LoanTransaction lo LocalDate payDate = switch (strategy) { case TILL_PRE_CLOSURE_DATE -> loanTransaction.getTransactionDate(); - case TILL_REST_FREQUENCY_DATE -> loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getFromDate()) // - && !loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getDueDate()) // - ? inAdvanceInstallment.getDueDate() // + case TILL_REST_FREQUENCY_DATE -> LoanRepaymentScheduleProcessingWrapper.isInPeriod(loanTransaction.getTransactionDate(), + inAdvanceInstallment.getFromDate(), inAdvanceInstallment.getDueDate(), false) ? inAdvanceInstallment.getDueDate() // : loanTransaction.getTransactionDate(); // case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE"); }; @@ -1639,10 +1648,11 @@ private Set getLoanChargesOfInstallment(Set charges, Loa .collect(Collectors.toSet()); } - private Money processPeriodsVertically(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, + private Money processPeriodsVertically(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, Set charges, Balances balances) { + MonetaryCurrency currency = ctx.getCurrency(); + List installments = ctx.getInstallments(); int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) { FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule(); @@ -1807,6 +1817,10 @@ private void handleReAge(LoanTransaction loanTransaction, TransactionCtx ctx) { reprocessInstallmentsOrder(installments); } + protected void calculateAccrualActivity(LoanTransaction transaction, TransactionCtx ctx) { + super.calculateAccrualActivity(transaction, ctx.getCurrency(), ctx.getInstallments()); + } + private void reprocessInstallmentsOrder(List installments) { AtomicInteger counter = new AtomicInteger(1); installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByDueDate) diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java index 1b7c10f3072..facb5d6c823 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java @@ -103,8 +103,7 @@ public void updateOutstandingLoanBalance() { if (isFirstInterestPeriod()) { Optional previousRepaymentPeriod = getRepaymentPeriod().getPrevious(); if (previousRepaymentPeriod.isPresent()) { - InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getInterestPeriods() - .get(previousRepaymentPeriod.get().getInterestPeriods().size() - 1); + InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getLastInterestPeriod(); this.outstandingLoanBalance = previousInterestPeriod.getOutstandingLoanBalance()// .plus(previousInterestPeriod.getDisbursementAmount(), mc)// .plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)// @@ -121,6 +120,6 @@ public void updateOutstandingLoanBalance() { } private boolean isFirstInterestPeriod() { - return getRepaymentPeriod().getInterestPeriods().get(0).equals(this); + return this.equals(getRepaymentPeriod().getFirstInterestPeriod()); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java index ad7acca6fa4..c096ee2c6cd 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java @@ -31,6 +31,7 @@ import lombok.experimental.Accessors; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; @Data @@ -83,7 +84,7 @@ public BigDecimal getInterestRate(final LocalDate effectiveDate) { private BigDecimal findInterestRate(final LocalDate effectiveDate) { return interestRates.stream() // - .filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate)) // + .filter(ir -> !DateUtils.isAfter(ir.effectiveFrom(), effectiveDate)) // .map(InterestRate::interestRate) // .findFirst() // .orElse(loanProductRelatedDetail.getAnnualNominalInterestRate()); // @@ -93,12 +94,12 @@ public void addInterestRate(final LocalDate newInterestEffectiveDate, final BigD interestRates.add(new InterestRate(newInterestEffectiveDate, newInterestRate)); } - public Optional findRepaymentPeriod(final LocalDate repaymentPeriodDueDate) { + public Optional findRepaymentPeriodByDueDate(final LocalDate repaymentPeriodDueDate) { if (repaymentPeriodDueDate == null) { return Optional.empty(); } return repaymentPeriods.stream()// - .filter(repaymentPeriodItem -> repaymentPeriodItem.getDueDate().isEqual(repaymentPeriodDueDate))// + .filter(repaymentPeriodItem -> DateUtils.isEqual(repaymentPeriodItem.getDueDate(), repaymentPeriodDueDate))// .findFirst(); } @@ -107,7 +108,7 @@ public List getRelatedRepaymentPeriods(final LocalDate calculat return repaymentPeriods; } return repaymentPeriods.stream()// - .filter(period -> !period.getDueDate().isBefore(calculateFromRepaymentPeriodDueDate))// + .filter(period -> !DateUtils.isBefore(period.getDueDate(), calculateFromRepaymentPeriodDueDate))// .toList();// } @@ -120,6 +121,10 @@ public int getLoanTermInDays() { return DateUtils.getExactDifferenceInDays(firstPeriod.getFromDate(), lastPeriod.getDueDate()); } + public LocalDate getStartDate() { + return !repaymentPeriods.isEmpty() ? repaymentPeriods.get(0).getFromDate() : null; + } + public LocalDate getMaturityDate() { return !repaymentPeriods.isEmpty() ? repaymentPeriods.get(repaymentPeriods.size() - 1).getDueDate() : null; } @@ -136,16 +141,8 @@ Optional findRepaymentPeriodForBalanceChange(final LocalDate ba return Optional.empty(); } return repaymentPeriods.stream()// - .filter(repaymentPeriod -> { - final boolean isFirstPeriod = repaymentPeriod.getPrevious().isEmpty(); - if (isFirstPeriod) { - return !balanceChangeDate.isBefore(repaymentPeriod.getFromDate()) - && !balanceChangeDate.isAfter(repaymentPeriod.getDueDate()); - } else { - return balanceChangeDate.isAfter(repaymentPeriod.getFromDate()) - && !balanceChangeDate.isAfter(repaymentPeriod.getDueDate()); - } - })// + .filter(repaymentPeriod -> LoanRepaymentScheduleProcessingWrapper.isInPeriod(balanceChangeDate, + repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate(), repaymentPeriod.getPrevious().isEmpty())) .findFirst(); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java index 6a531a90230..cf2193a41d4 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.data; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod; + +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; @@ -123,23 +126,6 @@ private Money calculateCalculatedDueInterest() { return calculatedDueInterest; } - private Money getZero(MathContext mc) { - // EMI is always initiated - return this.emi.zero(mc); - } - - public Money getCalculatedDuePrincipal() { - return getEmi().minus(getCalculatedDueInterest(), mc); - } - - public Money getTotalPaidAmount() { - return getPaidPrincipal().plus(getPaidInterest()); - } - - public boolean isFullyPaid() { - return getEmi().isEqualTo(getTotalPaidAmount()); - } - public Money getDueInterest() { if (dueInterestCalculation == null) { // Due interest might be the maximum paid if there is pay-off or early repayment @@ -150,11 +136,23 @@ public Money getDueInterest() { return dueInterestCalculation.get(); } + public Money getCalculatedDuePrincipal() { + return getEmi().minus(getCalculatedDueInterest(), mc); + } + public Money getDuePrincipal() { // Due principal might be the maximum paid if there is pay-off or early repayment return MathUtil.max(getEmi().minus(getDueInterest(), mc), getPaidPrincipal(), false); } + public Money getTotalPaidAmount() { + return getPaidPrincipal().plus(getPaidInterest()); + } + + public boolean isFullyPaid() { + return getEmi().isEqualTo(getTotalPaidAmount()); + } + public Money getUnrecognizedInterest() { return getCalculatedDueInterest().minus(getDueInterest(), mc); } @@ -193,4 +191,24 @@ public Money getInitialBalanceForEmiRecalculation() { (m1, m2) -> m1.plus(m2, mc)); return initialBalance.add(totalDisbursedAmount, mc); } + + private Money getZero(MathContext mc) { + // EMI is always initiated + return this.emi.zero(mc); + } + + public InterestPeriod getFirstInterestPeriod() { + return getInterestPeriods().get(0); + } + + public InterestPeriod getLastInterestPeriod() { + List interestPeriods = getInterestPeriods(); + return interestPeriods.get(interestPeriods.size() - 1); + } + + public Optional findInterestPeriod(@NotNull LocalDate transactionDate) { + return interestPeriods.stream()// + .filter(interestPeriod -> isInPeriod(transactionDate, fromDate, dueDate, false))// + .findFirst(); + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java index c11ee1fa98a..49e1b722bf9 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.findInPeriod; + +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; @@ -39,7 +42,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; @@ -47,6 +49,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; @@ -89,7 +92,7 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer // generate list of proposed schedule due dates final List expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc, periodStartDate, loanApplicationTerms, holidayDetailDTO); - final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generateInterestScheduleModel( + final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetail(), loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc); final List periods = new ArrayList<>(expectedRepaymentPeriods.size()); @@ -147,6 +150,63 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding); } + @Override + public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan, + HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, + LocalDate rescheduleFrom) { + LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO); + return LoanScheduleDTO.from(null, model); + } + + @Override + public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, + LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, + LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { + if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) { + throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor"); + } + + List installments = loan.getRepaymentScheduleInstallments(); + LoanRepaymentScheduleInstallment actualInstallment = findInPeriod(onDate, installments).orElse(installments.get(0)); + + LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { + case TILL_PRE_CLOSURE_DATE -> onDate; + case TILL_REST_FREQUENCY_DATE -> actualInstallment.getDueDate(); // due date of current installment + case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE"); + }; + + ProgressiveLoanInterestScheduleModel model = processor.calculateInterestScheduleModel(loan.getId(), onDate); + OutstandingDetails outstandingAmounts = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate); + // TODO: We should add all the past due outstanding amounts as well + OutstandingAmountsDTO result = new OutstandingAmountsDTO(currency) // + .principal(outstandingAmounts.getOutstandingPrincipal()) // + .interest(outstandingAmounts.getOutstandingInterest());// + + installments.forEach(installment -> result // + .plusFeeCharges(installment.getFeeChargesOutstanding(currency)) + .plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency))); + + return result; + } + + @Override + public Money getDueInterest(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate targetDate) { + return getDueAmounts(loan, installment, targetDate).getDueInterest(); + } + + @NotNull + PeriodDueDetails getDueAmounts(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment, + @NotNull LocalDate targetDate) { + LoanRepaymentScheduleTransactionProcessor transactionProcessor = loan.getTransactionProcessor(); + if (!(transactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) { + throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor"); + } + ProgressiveLoanInterestScheduleModel model = processor.calculateInterestScheduleModel(loan.getId(), targetDate); + return emiCalculator.getDueAmounts(model, installment.getDueDate(), targetDate); + } + + // Private, internal methods + private List getSortedDisbursementList(LoanApplicationTerms loanApplicationTerms) { final List disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas()); disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate)); @@ -238,49 +298,6 @@ private void processDisbursements(final LoanApplicationTerms loanApplicationTerm } } - @Override - public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan, - HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, - LocalDate rescheduleFrom) { - LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO); - return LoanScheduleDTO.from(null, model); - } - - @Override - public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, - LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, - LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { - if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) { - throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor"); - } - - List installments = loan.getRepaymentScheduleInstallments(); - LoanRepaymentScheduleInstallment actualInstallment = LoanRepaymentScheduleProcessingWrapper.findInPeriod(onDate, installments) - .orElse(installments.get(0)); - - LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { - case TILL_PRE_CLOSURE_DATE -> onDate; - case TILL_REST_FREQUENCY_DATE -> actualInstallment.getDueDate(); // due date of current installment - case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE"); - }; - - ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), onDate, - loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight(); - - OutstandingDetails result = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate); - // TODO: We should add all the past due outstanding amounts as well - OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) // - .principal(result.getOutstandingPrincipal()) // - .interest(result.getOutstandingInterest());// - - installments.forEach(installment -> amounts // - .plusFeeCharges(installment.getFeeChargesOutstanding(currency)) - .plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency))); - - return amounts; - } - - // Private, internal methods private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set loanCharges) { BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; if (loanCharges != null) { @@ -430,5 +447,4 @@ private Set separateTotalCompoundingPercentageCharges(final Set periods, - LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc); + @NotNull + ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List periods, + @NotNull LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc); - ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail, - Integer installmentAmountInMultiplesOf, List repaymentPeriods, MathContext mc); + @NotNull + ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel( + @NotNull List installments, @NotNull LoanProductRelatedDetail loanProductRelatedDetail, + Integer installmentAmountInMultiplesOf, MathContext mc); Optional findRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate dueDate); @@ -56,7 +60,9 @@ void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate r void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate, Money principalAmount); - PeriodDueDetails getDueAmounts(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate targetDate); + @NotNull + PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate, + @NotNull LocalDate targetDate); Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate, LocalDate targetDate); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index e33c2fec8f0..ae86e03a908 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -18,14 +18,18 @@ */ package org.apache.fineract.portfolio.loanproduct.calc; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod; + +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; import java.time.Year; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; @@ -41,7 +45,6 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; @Component @@ -52,20 +55,35 @@ public final class ProgressiveEMICalculator implements EMICalculator { private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7); @Override - public ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(final List periods, - final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) { - final Money zeroAmount = Money.zero(loanProductRelatedDetail.getCurrency(), mc); - final ArrayList interestRepaymentModelList = new ArrayList<>(periods.size()); - RepaymentPeriod previousPeriod = null; - for (final LoanScheduleModelRepaymentPeriod period : periods) { - RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, period.periodFromDate(), period.periodDueDate(), zeroAmount, - mc); - previousPeriod = currentPeriod; - interestRepaymentModelList.add(currentPeriod); + @NotNull + public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List periods, + @NotNull LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, + final MathContext mc) { + return generateInterestScheduleModel(periods, LoanScheduleModelRepaymentPeriod::periodFromDate, + LoanScheduleModelRepaymentPeriod::periodDueDate, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + } - } - return new ProgressiveLoanInterestScheduleModel(interestRepaymentModelList, loanProductRelatedDetail, - installmentAmountInMultiplesOf, mc); + @Override + @NotNull + public ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel( + @NotNull List installments, @NotNull LoanProductRelatedDetail loanProductRelatedDetail, + final Integer installmentAmountInMultiplesOf, final MathContext mc) { + return generateInterestScheduleModel(installments, LoanRepaymentScheduleInstallment::getFromDate, + LoanRepaymentScheduleInstallment::getDueDate, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + } + + @NotNull + private ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(@NotNull List periods, Function from, + Function to, @NotNull LoanProductRelatedDetail loanProductRelatedDetail, + final Integer installmentAmountInMultiplesOf, final MathContext mc) { + final Money zero = Money.zero(loanProductRelatedDetail.getCurrency()); + final AtomicReference prev = new AtomicReference<>(); + List repaymentPeriods = periods.stream().map(e -> { + RepaymentPeriod rp = new RepaymentPeriod(prev.get(), from.apply(e), to.apply(e), zero, mc); + prev.set(rp); + return rp; + }).toList(); + return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); } @Override @@ -74,7 +92,7 @@ public Optional findRepaymentPeriod(final ProgressiveLoanIntere if (scheduleModel == null) { return Optional.empty(); } - return scheduleModel.findRepaymentPeriod(repaymentPeriodDueDate); + return scheduleModel.findRepaymentPeriodByDueDate(repaymentPeriodDueDate); } /** @@ -138,13 +156,12 @@ public void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, Loca @Override public void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate, Money principalAmount) { + if (MathUtil.isEmpty(principalAmount)) { + return; + } Optional repaymentPeriod = findRepaymentPeriod(scheduleModel, repaymentPeriodDueDate); repaymentPeriod.ifPresent(rp -> rp.addPaidPrincipalAmount(principalAmount)); - LocalDate balanceCorrectionDate = transactionDate; - if (repaymentPeriodDueDate.isBefore(transactionDate)) { - // If it is paid late, we need to calculate with the period due date - balanceCorrectionDate = repaymentPeriodDueDate; - } + LocalDate balanceCorrectionDate = calcBalanceCorrectionDate(repaymentPeriodDueDate, transactionDate); addBalanceCorrection(scheduleModel, balanceCorrectionDate, principalAmount.negated()); repaymentPeriod.ifPresent(rp -> { @@ -157,12 +174,18 @@ public void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, Loc }); } + private static LocalDate calcBalanceCorrectionDate(LocalDate repaymentPeriodDueDate, LocalDate transactionDate) { + // If it is paid late, we need to calculate with the period due date + return DateUtils.isBefore(repaymentPeriodDueDate, transactionDate) ? repaymentPeriodDueDate : transactionDate; + } + @Override - public PeriodDueDetails getDueAmounts(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate, - final LocalDate targetDate) { + @NotNull + public PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate, + @NotNull LocalDate targetDate) { ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel, - repaymentPeriodDueDate, targetDate); - RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow(); + periodDueDate, targetDate); + RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow(); boolean multiplePeriodIsUnpaid = recalculatedScheduleModelTillDate.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()) .count() > 1L; if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) { @@ -175,11 +198,11 @@ public PeriodDueDetails getDueAmounts(final ProgressiveLoanInterestScheduleModel } @Override - public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, + public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate targetDate) { ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel, - repaymentPeriodDueDate, targetDate); - RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow(); + periodDueDate, targetDate); + RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow(); return repaymentPeriod.getOutstandingLoanBalance(); } @@ -189,12 +212,7 @@ public OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestS MathContext mc = scheduleModel.mc(); ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc); - scheduleModelCopy.repaymentPeriods().stream()// - .filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()// - .flatMap(rp -> rp.getInterestPeriods().stream()// - .filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) // - .reduce((one, two) -> two)) - .ifPresent(ip -> ip.setDueDate(targetDate)); // + findInterestPeriod(scheduleModelCopy, targetDate).ifPresent(ip -> ip.setDueDate(targetDate)); // calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy); scheduleModelCopy.repaymentPeriods() @@ -213,31 +231,24 @@ public OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestS } @NotNull - private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(ProgressiveLoanInterestScheduleModel scheduleModel, - LocalDate repaymentPeriodDueDate, LocalDate targetDate) { + private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate( + @NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate, @NotNull LocalDate targetDate) { MathContext mc = scheduleModel.mc(); ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc); - RepaymentPeriod repaymentPeriod = scheduleModelCopy.repaymentPeriods().stream() - .filter(rp -> rp.getDueDate().equals(repaymentPeriodDueDate)).findFirst().orElseThrow(); + RepaymentPeriod repaymentPeriod = scheduleModelCopy.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow(); LocalDate adjustedTargetDate = targetDate; InterestPeriod interestPeriod; if (!targetDate.isAfter(repaymentPeriod.getFromDate())) { - interestPeriod = repaymentPeriod.getInterestPeriods().get(0); + interestPeriod = repaymentPeriod.getFirstInterestPeriod(); adjustedTargetDate = repaymentPeriod.getFromDate(); } else if (targetDate.isAfter(repaymentPeriod.getDueDate())) { - interestPeriod = repaymentPeriod.getInterestPeriods().get(repaymentPeriod.getInterestPeriods().size() - 1); + interestPeriod = repaymentPeriod.getLastInterestPeriod(); adjustedTargetDate = repaymentPeriod.getDueDate(); } else { - interestPeriod = repaymentPeriod.getInterestPeriods().stream() - .filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())).findFirst().orElseThrow(); + interestPeriod = repaymentPeriod.findInterestPeriod(targetDate).orElseThrow(); } - scheduleModelCopy.repaymentPeriods().stream()// - .filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()// - .flatMap(rp -> rp.getInterestPeriods().stream()// - .filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) // - .reduce((one, two) -> two)) - .ifPresent(ip -> ip.setDueDate(targetDate)); // + findInterestPeriod(scheduleModelCopy, targetDate).ifPresent(ip -> ip.setDueDate(targetDate)); // interestPeriod.setDueDate(adjustedTargetDate); int index = repaymentPeriod.getInterestPeriods().indexOf(interestPeriod); repaymentPeriod.getInterestPeriods().subList(index + 1, repaymentPeriod.getInterestPeriods().size()).clear(); @@ -249,6 +260,15 @@ private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(Pr return scheduleModelCopy; } + @NotNull + private static Optional findInterestPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate targetDate) { + return scheduleModel.repaymentPeriods().stream()// + .filter(rp -> isInPeriod(targetDate, rp.getFromDate(), rp.getDueDate(), false)).findFirst()// + .flatMap(rp -> rp.getInterestPeriods().stream()// + .filter(ip -> isInPeriod(targetDate, ip.getFromDate(), ip.getDueDate(), false)) // + .reduce((one, two) -> two)); + } + /** * Calculate Equal Monthly Installment value and Rate Factor -1 values for calculate Interest */ @@ -698,22 +718,4 @@ BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal interestRate, fin BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal currentRateFactor, final MathContext mc) { return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, mc), mc); } - - @Override - public ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail, - Integer installmentAmountInMultiplesOf, List repaymentPeriods, MathContext mc) { - List repaymentModelsWithoutDownPayment = repaymentPeriods.stream() - .filter(period -> !period.isDownPayment() && !period.isAdditional()).toList(); - - List repaymentModels = new ArrayList<>(); - RepaymentPeriod previousPeriod = null; - for (LoanRepaymentScheduleInstallment repaymentModel : repaymentModelsWithoutDownPayment) { - RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, repaymentModel.getFromDate(), repaymentModel.getDueDate(), - Money.zero(repaymentModel.getLoan().getCurrency(), mc), mc); - previousPeriod = currentPeriod; - repaymentModels.add(currentPeriod); - } - - return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - } } diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java index d04ba491ed6..425211fec8a 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java @@ -60,6 +60,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; @@ -95,6 +96,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest { private static final MockedStatic MONEY_HELPER = mockStatic(MoneyHelper.class); private AdvancedPaymentScheduleTransactionProcessor underTest; private static final EMICalculator emiCalculator = Mockito.mock(EMICalculator.class); + private static final LoanRepositoryWrapper loanRepositoryWrapper = Mockito.mock(LoanRepositoryWrapper.class); @BeforeAll public static void init() { @@ -109,7 +111,7 @@ public static void destruct() { @BeforeEach public void setUp() { - underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator); + underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index 507b8ab08a6..22e3d4baa2c 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -158,7 +158,6 @@ public void test_fnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual @Test public void test_generateInterestScheduleModel() { - final List expectedRepaymentPeriods = new ArrayList<>(); final Integer installmentAmountInMultiplesOf = null; @@ -169,8 +168,8 @@ public void test_generateInterestScheduleModel() { Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator - .generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); Assertions.assertTrue(interestScheduleModel != null); Assertions.assertTrue(interestScheduleModel.loanProductRelatedDetail() != null); @@ -183,7 +182,6 @@ public void test_generateInterestScheduleModel() { @Test @Timeout(1) // seconds public void test_emi_calculator_performance() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -209,8 +207,8 @@ public void test_emi_calculator_performance() { Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -234,7 +232,6 @@ public void test_emi_calculator_performance() { @Test public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -254,8 +251,8 @@ public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() { Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -271,7 +268,6 @@ public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() { @Test public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -291,8 +287,8 @@ public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -308,7 +304,6 @@ public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() @Test public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -328,8 +323,8 @@ public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_ Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -355,7 +350,6 @@ public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_ @Test public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -375,8 +369,8 @@ public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEve Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -392,7 +386,6 @@ public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEve @Test public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -414,8 +407,8 @@ public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_d threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -435,7 +428,6 @@ public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_d @Test public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -457,8 +449,8 @@ public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt1 threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); - final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -482,7 +474,6 @@ public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt1 @Test public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -504,8 +495,8 @@ public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_d threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -529,7 +520,6 @@ public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_d */ @Test public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -551,8 +541,8 @@ public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInM threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -637,7 +627,6 @@ public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInM @Test public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -659,8 +648,8 @@ public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repay threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -676,9 +665,8 @@ public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repay Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getDuePrincipal())); Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getDueInterest())); - PeriodDueDetails details = null; - // check getdueAmounts forcast - details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); + // check getDueAmounts forcast + PeriodDueDetails details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); Assertions.assertEquals(16.52, toDouble(details.getDuePrincipal())); Assertions.assertEquals(0.49, toDouble(details.getDueInterest())); @@ -710,7 +698,6 @@ public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repay @Test public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -732,8 +719,8 @@ public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repay threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -809,8 +796,8 @@ public void test_multiDisbursedAmt300InSamePeriod_dayInYears360_daysInMonth30_re Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); Money disbursedAmount = toMoney(100); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -858,8 +845,8 @@ public void test_multiDisbursedAmt200InDifferentPeriod_dayInYears360_daysInMonth Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -907,8 +894,8 @@ public void test_multiDisbursedAmt150InSamePeriod_dayInYears360_daysInMonth30_re Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 5), disbursedAmount); @@ -951,8 +938,8 @@ public void test_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1M Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount); @@ -985,8 +972,8 @@ public void test_disbursedAmt1000_NoInterest_repayEvery1Month() { Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(1000.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1018,8 +1005,8 @@ public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery1Week Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1051,8 +1038,8 @@ public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery2Week Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1084,8 +1071,8 @@ public void test_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEver Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(15); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1116,8 +1103,8 @@ public void test_dailyInterest_disbursedAmt1000_dayInYears360_daysInMonth30_repa Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount = toMoney(1000.0); emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -1178,8 +1165,8 @@ public void test_dailyInterest_disbursedAmt2000_dayInYears360_daysInMonth30_repa Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); final Money disbursedAmount1st = toMoney(1000.0); final Money disbursedAmount2nd = toMoney(1000.0); diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java index 0d7e4902a20..fb9aa1e766d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java @@ -35,6 +35,7 @@ import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; import org.springframework.stereotype.Component; @Component @@ -50,69 +51,66 @@ public void createJournalEntriesForLoan(final LoanDTO loanDTO) { for (final LoanTransactionDTO loanTransactionDTO : loanDTO.getNewLoanTransactions()) { final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); this.helper.checkForBranchClosures(latestGLClosure, transactionDate); + final LoanTransactionEnumData transactionType = loanTransactionDTO.getTransactionType(); - /** Handle Disbursements **/ - if (loanTransactionDTO.getTransactionType().isDisbursement()) { + // Handle Disbursements + if (transactionType.isDisbursement()) { createJournalEntriesForDisbursements(loanDTO, loanTransactionDTO, office); } - /*** Handle Accruals ***/ - if (loanTransactionDTO.getTransactionType().isAccrual()) { + // Handle Accruals + if (transactionType.isAccrual() || transactionType.isAccrualAdjustment()) { createJournalEntriesForAccruals(loanDTO, loanTransactionDTO, office); } - /*** + /* * Handle repayments, loan refunds, repayments at disbursement and reversal of Repayments and Repayments at * disbursement (except charge adjustment) - ***/ - else if ((loanTransactionDTO.getTransactionType().isRepaymentType() - && !loanTransactionDTO.getTransactionType().isChargeAdjustment()) - || loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement() - || loanTransactionDTO.getTransactionType().isChargePayment()) { + */ + else if ((transactionType.isRepaymentType() && !transactionType.isChargeAdjustment()) + || transactionType.isRepaymentAtDisbursement() || transactionType.isChargePayment()) { createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, false, - loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement()); + transactionType.isRepaymentAtDisbursement()); } - /** Logic for handling recovery payments **/ - else if (loanTransactionDTO.getTransactionType().isRecoveryRepayment()) { + // Logic for handling recovery payments + else if (transactionType.isRecoveryRepayment()) { createJournalEntriesForRecoveryRepayments(loanDTO, loanTransactionDTO, office); } - /** Logic for Refunds of Overpayments **/ - else if (loanTransactionDTO.getTransactionType().isRefund()) { + // Logic for Refunds of Overpayments + else if (transactionType.isRefund()) { createJournalEntriesForRefund(loanDTO, loanTransactionDTO, office); } - /** Logic for Credit Balance Refunds **/ - else if (loanTransactionDTO.getTransactionType().isCreditBalanceRefund()) { + // Logic for Credit Balance Refunds + else if (transactionType.isCreditBalanceRefund()) { createJournalEntriesForCreditBalanceRefund(loanDTO, loanTransactionDTO, office); } - /** Handle Write Offs, waivers and their reversals **/ - else if ((loanTransactionDTO.getTransactionType().isWriteOff() || loanTransactionDTO.getTransactionType().isWaiveInterest() - || loanTransactionDTO.getTransactionType().isWaiveCharges())) { + // Handle Write Offs, waivers and their reversals + else if ((transactionType.isWriteOff() || transactionType.isWaiveInterest() || transactionType.isWaiveCharges())) { createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, true, false); } - /** Logic for Refunds of Active Loans **/ - else if (loanTransactionDTO.getTransactionType().isRefundForActiveLoans()) { + // Logic for Refunds of Active Loans + else if (transactionType.isRefundForActiveLoans()) { createJournalEntriesForRefundForActiveLoan(loanDTO, loanTransactionDTO, office); } // Logic for Chargebacks - else if (loanTransactionDTO.getTransactionType().isChargeback()) { + else if (transactionType.isChargeback()) { createJournalEntriesForChargeback(loanDTO, loanTransactionDTO, office); } // Logic for Charge Adjustment - else if (loanTransactionDTO.getTransactionType().isChargeAdjustment()) { + else if (transactionType.isChargeAdjustment()) { createJournalEntriesForChargeAdjustment(loanDTO, loanTransactionDTO, office); } // Logic for Charge-Off - else if (loanTransactionDTO.getTransactionType().isChargeoff()) { + else if (transactionType.isChargeoff()) { createJournalEntriesForChargeOff(loanDTO, loanTransactionDTO, office); } // Logic for Interest Payment Waiver - else if (loanTransactionDTO.getTransactionType().isInterestPaymentWaiver() - || loanTransactionDTO.getTransactionType().isInterestRefund()) { + else if (transactionType.isInterestPaymentWaiver() || transactionType.isInterestRefund()) { createJournalEntriesForInterestPaymentWaiverOrInterestRefund(loanDTO, loanTransactionDTO, office); } } @@ -1170,7 +1168,6 @@ private void createJournalEntriesForRecoveryRepayments(final LoanDTO loanDTO, fi * @param office */ private void createJournalEntriesForAccruals(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office) { - // loan properties final Long loanProductId = loanDTO.getLoanProductId(); final Long loanId = loanDTO.getLoanId(); @@ -1179,10 +1176,11 @@ private void createJournalEntriesForAccruals(final LoanDTO loanDTO, final LoanTr // transaction properties final String transactionId = loanTransactionDTO.getTransactionId(); final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); + final LoanTransactionEnumData transactionType = loanTransactionDTO.getTransactionType(); final BigDecimal interestAmount = loanTransactionDTO.getInterest(); final BigDecimal feesAmount = loanTransactionDTO.getFees(); final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties(); - final boolean isReversed = loanTransactionDTO.isReversed(); + final boolean isReversed = transactionType.isAccrualAdjustment() != loanTransactionDTO.isReversed(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); // create journal entries for recognizing interest (or reversal) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java index 35fb7e5e4f9..9298daed661 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java @@ -190,7 +190,7 @@ public Collection retrieveLoanApplicableFees() { } @Override - public Collection retrieveLoanAccountApplicableCharges(final Long loanId, ChargeTimeType[] excludeChargeTimes) { + public List retrieveLoanAccountApplicableCharges(final Long loanId, ChargeTimeType[] excludeChargeTimes) { final ChargeMapper rm = new ChargeMapper(); StringBuilder excludeClause = new StringBuilder(""); Map paramMap = new HashMap<>(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java index 3318a4d04ec..379366f928d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java @@ -42,6 +42,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.domain.CommandWrapper; @@ -442,7 +443,7 @@ private String retrieveLoanCharge(final Long loanId, final String loanExternalId final LoanChargeData loanCharge = this.loanChargeReadPlatformService.retrieveLoanChargeDetails(resolvedLoanChargeId, resolvedLoanId); - final Collection installmentChargeData = this.loanChargeReadPlatformService + final List installmentChargeData = this.loanChargeReadPlatformService .retrieveInstallmentLoanCharges(resolvedLoanChargeId, true); final LoanChargeData loanChargeData = new LoanChargeData(loanCharge, installmentChargeData); @@ -536,7 +537,7 @@ private String retrieveTemplate(final Long loanId, final String loanExternalIdSt ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr); Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId); - final Collection chargeOptions = this.chargeReadPlatformService.retrieveLoanAccountApplicableCharges(resolvedLoanId, + final List chargeOptions = this.chargeReadPlatformService.retrieveLoanAccountApplicableCharges(resolvedLoanId, new ChargeTimeType[] { ChargeTimeType.OVERDUE_INSTALLMENT }); final LoanChargeData loanChargeTemplate = LoanChargeData.template(chargeOptions); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java index 208f33532ad..b0845727626 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java @@ -46,19 +46,19 @@ public class AddAccrualEntriesTasklet implements Tasklet { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { Collection loanScheduleAccrualDataList = loanReadPlatformService.retriveScheduleAccrualData(); - Map> loanDataMap = new HashMap<>(); + Map> loanDataMap = new HashMap<>(); for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDataList) { if (loanDataMap.containsKey(accrualData.getLoanId())) { loanDataMap.get(accrualData.getLoanId()).add(accrualData); } else { - Collection accrualDataList = new ArrayList<>(); + List accrualDataList = new ArrayList<>(); accrualDataList.add(accrualData); loanDataMap.put(accrualData.getLoanId(), accrualDataList); } } List errors = new ArrayList<>(); - for (Map.Entry> mapEntry : loanDataMap.entrySet()) { + for (Map.Entry> mapEntry : loanDataMap.entrySet()) { try { loanAccrualsProcessingService.addAccrualAccounting(mapEntry.getKey(), mapEntry.getValue()); } catch (Exception e) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java index 9a730875101..add20430ea6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -18,8 +18,14 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isAfterPeriod; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isBeforePeriod; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrualAdjustment; import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrueTransaction; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; @@ -34,6 +40,7 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.domain.ExternalId; @@ -46,26 +53,24 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; -import org.apache.fineract.organisation.monetary.data.CurrencyData; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.office.domain.OfficeRepository; -import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.loanaccount.data.AccrualAmountsData; +import org.apache.fineract.portfolio.loanaccount.data.AccrualChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; -import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; +import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; @@ -73,11 +78,14 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod; -import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataAccessException; import org.springframework.stereotype.Component; @@ -104,16 +112,27 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing private final LoanRepository loanRepository; private final OfficeRepository officeRepository; private final LoanChargeRepository loanChargeRepository; + private final LoanScheduleGeneratorFactory loanScheduleFactory; /** * method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan */ @Override @Transactional - public void addPeriodicAccruals(final LocalDate tillDate) throws JobExecutionException { - Collection loanScheduleAccrualDataList = this.loanReadPlatformService - .retrievePeriodicAccrualData(tillDate); - addPeriodicAccruals(tillDate, loanScheduleAccrualDataList); + public void addPeriodicAccruals(@NotNull LocalDate tillDate) throws JobExecutionException { + List loans = loanRepositoryWrapper.findLoansForAccrual(AccountingRuleType.ACCRUAL_PERIODIC.getValue(), tillDate); + List errors = new ArrayList<>(); + for (Loan loan : loans) { + try { + addPeriodicAccruals(tillDate, loan); + } catch (Exception e) { + log.error("Failed to add accrual for loan {}", loan.getId(), e); + errors.add(e); + } + } + if (!errors.isEmpty()) { + throw new JobExecutionException(errors); + } } /** @@ -121,36 +140,277 @@ public void addPeriodicAccruals(final LocalDate tillDate) throws JobExecutionExc */ @Override @Transactional - public void addPeriodicAccruals(final LocalDate tillDate, Loan loan) throws JobExecutionException { - Collection loanScheduleAccrualDataList = this.loanReadPlatformService.retrievePeriodicAccrualData(tillDate, - loan); - addPeriodicAccruals(tillDate, loanScheduleAccrualDataList); - } - - private void addPeriodicAccruals(final LocalDate tillDate, Collection loanScheduleAccrualDataList) - throws JobExecutionException { - Map> loanDataMap = new HashMap<>(); - for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDataList) { - if (loanDataMap.containsKey(accrualData.getLoanId())) { - loanDataMap.get(accrualData.getLoanId()).add(accrualData); + public void addPeriodicAccruals(@NotNull LocalDate tillDate, @NotNull Loan loan) throws JobExecutionException { + if (!loan.isOpen() || loan.isNpa() || loan.isChargedOff() || !loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + return; + } + LoanInterestRecalculationDetails recalculationDetails = loan.getLoanInterestRecalculationDetails(); + if (recalculationDetails != null && recalculationDetails.isCompoundingToBePostedAsTransaction()) { + return; + } + boolean progressiveAccrual = isProgressiveAccrual(loan); + MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency(); + + List accrualAmountsList = calculateAccrualAmounts(loan, tillDate); + List accrualTransactions = new ArrayList<>(); + LoanTransaction progressiveAccrualTransaction = null; + LoanTransaction progressiveAdjustTransaction = null; + for (AccrualAmountsData accrualAmounts : accrualAmountsList) { + Money interestAccruable = accrualAmounts.getInterestAccruable(); + Money interestPortion = MathUtil.minus(interestAccruable, accrualAmounts.getInterestAccrued()); + Money feeAccruable = accrualAmounts.getFeeAccruable(); + Money feePortion = MathUtil.minus(feeAccruable, accrualAmounts.getFeeAccrued()); + Money penaltyAccruable = accrualAmounts.getPenaltyAccruable(); + Money penaltyPortion = MathUtil.minus(penaltyAccruable, accrualAmounts.getPenaltyAccrued()); + if (MathUtil.isEmpty(interestPortion) && MathUtil.isEmpty(feePortion) && MathUtil.isEmpty(penaltyPortion)) { + continue; + } + if (progressiveAccrual) { + Money interestAdjustmentPortion = MathUtil.negate(interestPortion); + Money feeAdjustmentPortion = MathUtil.negate(feePortion); + Money penaltyAdjustmentPortion = MathUtil.negate(penaltyPortion); + if (progressiveAdjustTransaction == null) { + progressiveAdjustTransaction = addAccrualTransaction(loan, tillDate, accrualAmounts, interestAdjustmentPortion, + feeAdjustmentPortion, penaltyAdjustmentPortion, true); + if (progressiveAdjustTransaction != null) { + accrualTransactions.add(progressiveAdjustTransaction); + } + } else { + mergeAccrualTransaction(progressiveAdjustTransaction, accrualAmounts, interestAdjustmentPortion, feeAdjustmentPortion, + penaltyAdjustmentPortion, true); + } + if (progressiveAccrualTransaction == null) { + progressiveAccrualTransaction = addAccrualTransaction(loan, tillDate, accrualAmounts, interestPortion, feePortion, + penaltyPortion, false); + if (progressiveAccrualTransaction != null) { + accrualTransactions.add(progressiveAccrualTransaction); + } + } else { + mergeAccrualTransaction(progressiveAccrualTransaction, accrualAmounts, interestPortion, feePortion, penaltyPortion, + false); + } } else { - Collection accrualDataList = new ArrayList<>(); - accrualDataList.add(accrualData); - loanDataMap.put(accrualData.getLoanId(), accrualDataList); + LoanTransaction accrualTransaction = addAccrualTransaction(loan, tillDate, accrualAmounts, interestPortion, feePortion, + penaltyPortion, false); + if (accrualTransaction != null) { + accrualTransactions.add(accrualTransaction); + } } + LoanRepaymentScheduleInstallment installment = loan.fetchRepaymentScheduleInstallment(accrualAmounts.getInstallmentNumber()); + installment.updateAccrualPortion(interestAccruable, feeAccruable, penaltyAccruable); } + ArrayList> newTransactionMapping = new ArrayList<>(); + for (LoanTransaction accrualTransaction : accrualTransactions) { + loanTransactionRepository.save(accrualTransaction); + newTransactionMapping.add(accrualTransaction.toMapData(currency.getCode())); + businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction)); + } + loan.setAccruedTill(tillDate); + loanRepository.saveAndFlush(loan); - List errors = new ArrayList<>(); - for (Map.Entry> mapEntry : loanDataMap.entrySet()) { - try { - addPeriodicAccruals(tillDate, mapEntry.getKey(), mapEntry.getValue()); - } catch (Exception e) { - log.error("Failed to add accrual transaction for loan {}", mapEntry.getKey(), e); - errors.add(e); - } + Map accountingBridgeData = deriveAccountingBridgeData(loan, newTransactionMapping); + this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); + } + + private List calculateAccrualAmounts(@NotNull Loan loan, @NotNull LocalDate tillDate) { + final String chargeAccrualDateType = configurationDomainService.getAccrualDateConfigForCharge(); + boolean chargeOnDueDate = ACCRUAL_ON_CHARGE_DUE_DATE.equalsIgnoreCase(chargeAccrualDateType); + + LoanProductRelatedDetail productDetail = loan.getLoanProductRelatedDetail(); + MonetaryCurrency currency = productDetail.getCurrency(); + LoanScheduleGenerator scheduleGenerator = loanScheduleFactory.create(productDetail.getLoanScheduleType(), + productDetail.getInterestMethod()); + int firstInstallmentNumber = fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments()); + List installments = getInstallmentsToAccrue(loan, tillDate); + ArrayList accrualAmounts = new ArrayList<>(installments.size()); + for (LoanRepaymentScheduleInstallment installment : installments) { + Integer installmentNumber = installment.getInstallmentNumber(); + AccrualAmountsData accrualData = new AccrualAmountsData(installmentNumber, currency); + accrualAmounts.add(accrualData); + boolean isFirst = installmentNumber.equals(firstInstallmentNumber); + addInterestAccrual(loan, tillDate, scheduleGenerator, installment, isFirst, accrualData); + addChargeAccrual(loan, tillDate, chargeOnDueDate, installment, isFirst, accrualData); + } + return accrualAmounts; + } + + @NotNull + private List getInstallmentsToAccrue(@NotNull Loan loan, @NotNull LocalDate tillDate) { + LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate(); + int firstInstallmentNumber = fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments()); + return loan + .getRepaymentScheduleInstallments(i -> !isBeforePeriod(tillDate, i, i.getInstallmentNumber().equals(firstInstallmentNumber)) + && !isAfterPeriod(organisationStartDate, i) + && (!MathUtil.isEqualTo(i.getInterestCharged(), i.getInterestAccrued()) + || !MathUtil.isEqualTo(i.getFeeChargesCharged(), i.getFeeAccrued()) + || !MathUtil.isEqualTo(i.getPenaltyCharges(), i.getPenaltyAccrued()))); + } + + private void addInterestAccrual(@NotNull Loan loan, @NotNull LocalDate tillDate, LoanScheduleGenerator scheduleGenerator, + LoanRepaymentScheduleInstallment installment, boolean isFirstPeriod, AccrualAmountsData accrualData) { + Money interest = null; + if (isInPeriod(tillDate, installment, isFirstPeriod)) { + interest = scheduleGenerator.getDueInterest(loan, installment, tillDate); + } else if (isAfterPeriod(tillDate, installment)) { + interest = installment.getInterestCharged(accrualData.getCurrency()); + } + accrualData.setInterestAmount(interest); + MonetaryCurrency currency = accrualData.getCurrency(); + Money waived = Money.of(currency, calcInterestWaivedAmount(installment, tillDate)); + accrualData.setInterestAccruable(MathUtil.minusToZero(accrualData.getInterestAmount(), waived)); + accrualData.setInterestAccrued(Money.of(currency, calcInterestAccruedAmount(installment))); + } + + @NotNull + private static BigDecimal calcInterestAccruedAmount(@NotNull LoanRepaymentScheduleInstallment installment) { + return installment.getLoanTransactionToRepaymentScheduleMappings().stream().filter(tm -> { + LoanTransaction t = tm.getLoanTransaction(); + return !t.isReversed() && (t.isAccrual() || t.isAccrualAdjustment()); + }).map(tm -> tm.getLoanTransaction().isAccrual() ? tm.getInterestPortion() : MathUtil.negate(tm.getInterestPortion())) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + @NotNull + private static BigDecimal calcInterestWaivedAmount(@NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate tillDate) { + return installment.getLoanTransactionToRepaymentScheduleMappings().stream().filter(tm -> { + LoanTransaction t = tm.getLoanTransaction(); + return !t.isReversed() && t.isInterestWaiver() && !DateUtils.isAfter(t.getTransactionDate(), tillDate); + }).map(tm -> tm.getLoanTransaction().isAccrual() ? tm.getInterestPortion() : MathUtil.negate(tm.getInterestPortion())) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private void addChargeAccrual(@NotNull Loan loan, @NotNull LocalDate tillDate, boolean chargeOnDueDate, + LoanRepaymentScheduleInstallment installment, boolean isFirstPeriod, AccrualAmountsData accrualData) { + LocalDate dueDate = installment.getDueDate(); + List loanCharges = loan.getLoanCharges(lc -> lc.isInstalmentFee() ? DateUtils.isEqual(tillDate, dueDate) + : isChargeDue(lc, tillDate, chargeOnDueDate, installment, isFirstPeriod)); + for (LoanCharge loanCharge : loanCharges) { + addChargeAccrual(loanCharge, loanCharge.isInstalmentFee() ? installment : null, accrualData); } - if (!errors.isEmpty()) { - throw new JobExecutionException(errors); + } + + private void addChargeAccrual(@NotNull LoanCharge loanCharge, LoanRepaymentScheduleInstallment installment, + @NotNull AccrualAmountsData accrualData) { + MonetaryCurrency currency = accrualData.getCurrency(); + Money chargeAmount; + Collection paidBy; + Long installmentChargeId = null; + if (installment == null) { + chargeAmount = loanCharge.getAmount(currency); + paidBy = loanCharge.getLoanChargePaidBySet(); + } else { + LoanInstallmentCharge installmentCharge = loanCharge.getInstallmentLoanCharge(installment.getInstallmentNumber()); + if (installmentCharge == null) { + return; + } + chargeAmount = installmentCharge.getAmount(currency); + paidBy = loanCharge.getLoanChargePaidBy(pb -> installment.getInstallmentNumber().equals(pb.getInstallmentNumber())); + installmentChargeId = installmentCharge.getId(); + } + AccrualChargeData chargeData = new AccrualChargeData(loanCharge.getId(), installmentChargeId, loanCharge.isPenaltyCharge()) + .setChargeAmount(chargeAmount); + accrualData.addCharge(chargeData); + Money unrecognized = Money.of(currency, calcChargeUnrecognizedWaiverAmount(paidBy)); + chargeData.setChargeAccruable(MathUtil.minusToZero(chargeData.getChargeAmount(), unrecognized)); + chargeData.setChargeAccrued(Money.of(currency, calcChargeAccruedAmount(paidBy))); + } + + @NotNull + private static BigDecimal calcChargeUnrecognizedWaiverAmount(@NotNull Collection loanChargePaidBy) { + return loanChargePaidBy.stream() // TODO only for cumulative Loan + .filter(pb -> { + LoanTransaction t = pb.getLoanTransaction(); + return !t.isReversed() && t.isWaiveCharge(); + }).map(pb -> pb.getLoanTransaction().getUnrecognizedIncomePortion()).reduce(BigDecimal.ZERO, BigDecimal::add); + } + + @NotNull + private static BigDecimal calcChargeAccruedAmount(@NotNull Collection loanChargePaidBy) { + return loanChargePaidBy.stream().filter(pb -> { + LoanTransaction t = pb.getLoanTransaction(); + return !t.isReversed() && (t.isAccrual() || t.isAccrualAdjustment()); + }).map(pb -> pb.getLoanTransaction().isAccrual() ? pb.getAmount() : MathUtil.negate(pb.getAmount())).reduce(BigDecimal.ZERO, + BigDecimal::add); + } + + private static boolean isChargeDue(@NotNull LoanCharge loanCharge, @NotNull LocalDate tillDate, boolean chargeOnDueDate, + LoanRepaymentScheduleInstallment installment, boolean isFirstPeriod) { + LocalDate fromDate = installment.getFromDate(); + LocalDate dueDate = installment.getDueDate(); + LocalDate toDate = DateUtils.isBefore(dueDate, tillDate) ? dueDate : tillDate; + if (chargeOnDueDate) { + return loanCharge.isDueInPeriod(fromDate, toDate, isFirstPeriod); + } else { + LocalDate submittedOnDate = loanCharge.getSubmittedOnDate(); + // TODO not correct, what if submittedOnDate and chargeDueDate are in different periods + // LocalDate chargeDueDate = loanCharge.getDueDate(); + // return ((isFirstPeriod && DateUtils.isEqual(submittedOnDate, fromDate) && + // DateUtils.isEqual(chargeDueDate, fromDate)) || DateUtils.isAfter(chargeDueDate, fromDate)) + // && !DateUtils.isAfter(submittedOnDate, toDate) + // && !DateUtils.isAfter(chargeDueDate, dueDate); + return isInPeriod(submittedOnDate, fromDate, toDate, isFirstPeriod); + } + } + + private LoanTransaction addAccrualTransaction(@NotNull Loan loan, @NotNull LocalDate tillDate, AccrualAmountsData accrualAmounts, + Money interestPortion, Money feePortion, Money penaltyPortion, boolean adjustment) { + interestPortion = MathUtil.negativeToZero(interestPortion); + BigDecimal interest = MathUtil.toBigDecimal(interestPortion); + feePortion = MathUtil.negativeToZero(feePortion); + BigDecimal fee = MathUtil.toBigDecimal(feePortion); + penaltyPortion = MathUtil.negativeToZero(penaltyPortion); + BigDecimal penalty = MathUtil.toBigDecimal(penaltyPortion); + BigDecimal amount = MathUtil.add(interest, fee, penalty); + if (!MathUtil.isGreaterThanZero(amount)) { + return null; + } + LoanTransaction transaction = adjustment + ? accrualAdjustment(loan, loan.getOffice(), tillDate, amount, interest, fee, penalty, externalIdFactory.create()) + : accrueTransaction(loan, loan.getOffice(), tillDate, amount, interest, fee, penalty, externalIdFactory.create()); + loan.addLoanTransaction(transaction); + + // update repayment schedule portions + addTransactionMappings(transaction, accrualAmounts, interestPortion, feePortion, penaltyPortion, adjustment); + return transaction; + } + + private void mergeAccrualTransaction(@NotNull LoanTransaction transaction, AccrualAmountsData accrualAmounts, Money interestPortion, + Money feePortion, Money penaltyPortion, boolean adjustment) { + interestPortion = MathUtil.negativeToZero(interestPortion); + feePortion = MathUtil.negativeToZero(feePortion); + penaltyPortion = MathUtil.negativeToZero(penaltyPortion); + + transaction.updateComponentsAndTotal(null, interestPortion, feePortion, penaltyPortion); + addTransactionMappings(transaction, accrualAmounts, interestPortion, feePortion, penaltyPortion, adjustment); + } + + private static void addTransactionMappings(@NotNull LoanTransaction transaction, AccrualAmountsData accrualAmounts, + Money interestPortion, Money feePortion, Money penaltyPortion, boolean adjustment) { + Loan loan = transaction.getLoan(); + // update repayment schedule portions + Integer installmentNumber = accrualAmounts.getInstallmentNumber(); + LoanRepaymentScheduleInstallment installment = loan.fetchRepaymentScheduleInstallment(installmentNumber); + + // add installment mapping + LoanTransactionToRepaymentScheduleMapping installmentMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(transaction, + installment, null, interestPortion, feePortion, penaltyPortion); + installment.getLoanTransactionToRepaymentScheduleMappings().add(installmentMapping); + + // add charges paid by mappings + for (AccrualChargeData accrualCharge : accrualAmounts.getCharges()) { + Money chargeAccruable = accrualCharge.getChargeAccruable(); + Money chargePortion = MathUtil.minus(chargeAccruable, accrualCharge.getChargeAccrued()); + chargePortion = MathUtil.negativeToZero(adjustment ? MathUtil.negate(chargePortion) : chargePortion); + if (MathUtil.isEmpty(chargePortion)) { + continue; + } + BigDecimal chargeAmount = MathUtil.toBigDecimal(chargePortion); + LoanCharge loanCharge = loan.fetchLoanChargesById(accrualCharge.getLoanChargeId()); + transaction.getLoanChargesPaid().add(new LoanChargePaidBy(transaction, loanCharge, chargeAmount, installmentNumber)); + Long installmentChargeId = accrualCharge.getLoanInstallmentChargeId(); + if (installmentChargeId != null) { + loanCharge.getLoanInstallmentCharge().add(new LoanInstallmentCharge(chargeAmount, loanCharge, installment)); + } } } @@ -159,61 +419,30 @@ private void addPeriodicAccruals(final LocalDate tillDate, Collection loanScheduleAccrualData) { - Collection chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId); - Collection loanWaiverScheduleData = new ArrayList<>(1); - Collection loanWaiverTransactionData = new ArrayList<>(1); + public void addAccrualAccounting(@NotNull Long loanId, @NotNull List loanScheduleAccrualData) { + // TODO + Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); + List chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId); + List loanWaiverScheduleData = new ArrayList<>(1); + List loanWaiverTransactionData = new ArrayList<>(1); for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualData) { if (accrualData.getWaivedInterestIncome() != null && loanWaiverScheduleData.isEmpty()) { loanWaiverScheduleData = this.loanReadPlatformService.fetchWaiverInterestRepaymentData(accrualData.getLoanId()); loanWaiverTransactionData = this.loanReadPlatformService.retrieveWaiverLoanTransactions(accrualData.getLoanId()); } updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate()); - updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, accrualData.getDueDateAsLocaldate()); - calculateFinalAccrualsForScheduleAndAddAccrualAccounting(accrualData); - } - } - - private void addPeriodicAccruals(final LocalDate tillDate, Long loanId, Collection loanScheduleAccrualData) { - boolean firstTime = true; - LocalDate accruedTill = null; - Collection chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId); - Collection loanWaiverScheduleData = new ArrayList<>(1); - Collection loanWaiverTransactionData = new ArrayList<>(1); - for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualData) { - if (accrualData.getWaivedInterestIncome() != null && loanWaiverScheduleData.isEmpty()) { - loanWaiverScheduleData = this.loanReadPlatformService.fetchWaiverInterestRepaymentData(accrualData.getLoanId()); - loanWaiverTransactionData = this.loanReadPlatformService.retrieveWaiverLoanTransactions(accrualData.getLoanId()); - } - - if (DateUtils.isAfter(accrualData.getDueDateAsLocaldate(), tillDate)) { - if (accruedTill == null || firstTime) { - accruedTill = accrualData.getAccruedTill(); - firstTime = false; - } - if (accruedTill == null || DateUtils.isBefore(accruedTill, tillDate)) { - updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), tillDate); - updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, tillDate); - calculateFinalAccrualsForScheduleTillSpecificDateAndAddAccrualAccounting(tillDate, accrualData); - } - } else { - updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate()); - updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, tillDate); - calculateFinalAccrualsForScheduleAndAddAccrualAccounting(accrualData); - accruedTill = accrualData.getDueDateAsLocaldate(); - } + updateInterestIncome(loan, accrualData, loanWaiverTransactionData, loanWaiverScheduleData, accrualData.getDueDateAsLocaldate()); + calculateFinalAccrualsForScheduleAndAddAccrualAccounting(loan, accrualData); } } @Transactional @Override public void addIncomeAndAccrualTransactions(Long loanId) throws LoanNotFoundException { + // TODO if (loanId != null) { Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); - if (loan == null) { - throw new LoanNotFoundException(loanId); - } final List existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); processIncomePostingAndAccruals(loan); @@ -228,8 +457,11 @@ public void addIncomeAndAccrualTransactions(Long loanId) throws LoanNotFoundExce * reschedule */ @Override - public void reprocessExistingAccruals(Loan loan) { - Collection accruals = retrieveListOfAccrualTransactions(loan); + public void reprocessExistingAccruals(@NotNull Loan loan) { + if (isProgressiveAccrual(loan)) { + return; + } + List accruals = retrieveListOfAccrualTransactions(loan); if (!accruals.isEmpty()) { if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { reprocessPeriodicAccruals(loan, accruals); @@ -244,60 +476,58 @@ public void reprocessExistingAccruals(Loan loan) { */ @Override @Transactional - public void processAccrualsForInterestRecalculation(Loan loan, boolean isInterestRecalculationEnabled) { + public void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled) { + if (isProgressiveAccrual(loan)) { + return; + } LocalDate accruedTill = loan.getAccruedTill(); - if (!loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() || !isInterestRecalculationEnabled || accruedTill == null - || loan.isNpa() || !loan.getStatus().isActive() || loan.isChargedOff()) { + if (!isInterestRecalculationEnabled || accruedTill == null) { return; } - - Collection loanScheduleAccrualList = new ArrayList<>(); - accruedTill = createLoanScheduleAccrualDataList(loan, accruedTill, loanScheduleAccrualList); - - if (!loanScheduleAccrualList.isEmpty()) { - try { - addPeriodicAccruals(accruedTill, loanScheduleAccrualList); - } catch (MultiException e) { - String globalisationMessageCode = "error.msg.accrual.exception"; - throw new GeneralPlatformDomainRuleException(globalisationMessageCode, e.getMessage(), e); - } + try { + addPeriodicAccruals(accruedTill, loan); + } catch (MultiException e) { + String globalisationMessageCode = "error.msg.accrual.exception"; + throw new GeneralPlatformDomainRuleException(globalisationMessageCode, e.getMessage(), e); } - } /** * method calculates accruals for loan with interest recalculation and compounding to be posted as income */ @Override - public void processIncomePostingAndAccruals(Loan loan) { - if (loan.getLoanInterestRecalculationDetails() != null - && loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction()) { - LocalDate lastCompoundingDate = loan.getDisbursementDate(); - List compoundingDetails = extractInterestRecalculationAdditionalDetails(loan); - List incomeTransactions = retrieveListOfIncomePostingTransactions(loan); - List accrualTransactions = retrieveListOfAccrualTransactions(loan); - for (LoanInterestRecalcualtionAdditionalDetails compoundingDetail : compoundingDetails) { - if (!DateUtils.isBeforeBusinessDate(compoundingDetail.getEffectiveDate())) { - break; - } - LoanTransaction incomeTransaction = getTransactionForDate(incomeTransactions, compoundingDetail.getEffectiveDate()); - LoanTransaction accrualTransaction = getTransactionForDate(accrualTransactions, compoundingDetail.getEffectiveDate()); - addUpdateIncomeAndAccrualTransaction(loan, compoundingDetail, lastCompoundingDate, incomeTransaction, accrualTransaction); - lastCompoundingDate = compoundingDetail.getEffectiveDate(); + public void processIncomePostingAndAccruals(@NotNull Loan loan) { + if (isProgressiveAccrual(loan)) { + return; + } + LoanInterestRecalculationDetails recalculationDetails = loan.getLoanInterestRecalculationDetails(); + if (recalculationDetails == null || !recalculationDetails.isCompoundingToBePostedAsTransaction()) { + return; + } + LocalDate lastCompoundingDate = loan.getDisbursementDate(); + List compoundingDetails = extractInterestRecalculationAdditionalDetails(loan); + List incomeTransactions = retrieveListOfIncomePostingTransactions(loan); + List accrualTransactions = retrieveListOfAccrualTransactions(loan); + for (LoanInterestRecalcualtionAdditionalDetails compoundingDetail : compoundingDetails) { + if (!DateUtils.isBeforeBusinessDate(compoundingDetail.getEffectiveDate())) { + break; } - List installments = loan.getRepaymentScheduleInstallments(); - LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment - .getLastNonDownPaymentInstallment(installments); - reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate()); - reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate()); + LoanTransaction incomeTransaction = getTransactionForDate(incomeTransactions, compoundingDetail.getEffectiveDate()); + LoanTransaction accrualTransaction = getTransactionForDate(accrualTransactions, compoundingDetail.getEffectiveDate()); + addUpdateIncomeAndAccrualTransaction(loan, compoundingDetail, lastCompoundingDate, incomeTransaction, accrualTransaction); + lastCompoundingDate = compoundingDetail.getEffectiveDate(); } + List installments = loan.getRepaymentScheduleInstallments(); + LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment.getLastNonDownPaymentInstallment(installments); + reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate()); + reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate()); } /** * method calculates accruals for loan on loan closure */ @Override - public void processAccrualsForLoanClosure(Loan loan) { + public void processAccrualsForLoanClosure(@NotNull Loan loan) { // check and process accruals for loan WITHOUT interest recalculation details and compounding posted as income processAccrualTransactionsOnLoanClosure(loan); @@ -309,8 +539,9 @@ public void processAccrualsForLoanClosure(Loan loan) { * method calculates accruals for loan on loan fore closure */ @Override - public void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDate, - Collection newAccrualTransactions) { + public void processAccrualsForLoanForeClosure(@NotNull Loan loan, @NotNull LocalDate foreClosureDate, + @NotNull List newAccrualTransactions) { + // TODO analyze progressive accrual case if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() && (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) { final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); @@ -333,222 +564,108 @@ public void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDa } } - private void calculateFinalAccrualsForScheduleTillSpecificDateAndAddAccrualAccounting(final LocalDate tillDate, - final LoanScheduleAccrualData accrualData) { - - BigDecimal amount = BigDecimal.ZERO; - BigDecimal feePortion = accrualData.getDueDateFeeIncome(); - BigDecimal penaltyPortion = accrualData.getDueDatePenaltyIncome(); - BigDecimal interestPortion = getInterestAccruedTillDate(tillDate, accrualData); - - BigDecimal totalAccInterest = accrualData.getAccruedInterestIncome(); - BigDecimal totalAccPenalty = accrualData.getAccruedPenaltyIncome(); - BigDecimal totalCreditedPenalty = accrualData.getCreditedPenalty(); - BigDecimal totalAccFee = accrualData.getAccruedFeeIncome(); - BigDecimal totalCreditedFee = accrualData.getCreditedFee(); - + private void calculateFinalAccrualsForScheduleAndAddAccrualAccounting(@NotNull Loan loan, LoanScheduleAccrualData accrualData) { // interest - if (totalAccInterest == null) { - totalAccInterest = BigDecimal.ZERO; - } - interestPortion = interestPortion.subtract(totalAccInterest); - amount = amount.add(interestPortion); - totalAccInterest = totalAccInterest.add(interestPortion); - if (interestPortion.compareTo(BigDecimal.ZERO) == 0) { - interestPortion = null; - } + BigDecimal newAccruedInterest = MathUtil.zeroToNull(accrualData.getAccruableIncome()); + BigDecimal interestPortion = MathUtil.zeroToNull(MathUtil.subtract(newAccruedInterest, accrualData.getAccruedInterestIncome())); // fee - if (feePortion != null) { - if (totalAccFee == null) { - totalAccFee = BigDecimal.ZERO; - } - if (totalCreditedFee == null) { - totalCreditedFee = BigDecimal.ZERO; - } - feePortion = feePortion.subtract(totalAccFee).subtract(totalCreditedFee); - amount = amount.add(feePortion); - totalAccFee = totalAccFee.add(feePortion); - if (feePortion.compareTo(BigDecimal.ZERO) == 0) { - feePortion = null; - } - } + BigDecimal newAccruedFee = MathUtil.zeroToNull(accrualData.getDueDateFeeIncome()); + BigDecimal feePortion = MathUtil + .zeroToNull(MathUtil.subtract(newAccruedFee, accrualData.getAccruedFeeIncome(), accrualData.getCreditedFee())); // penalty - if (penaltyPortion != null) { - if (totalAccPenalty == null) { - totalAccPenalty = BigDecimal.ZERO; - } - if (totalCreditedPenalty == null) { - totalCreditedPenalty = BigDecimal.ZERO; - } - penaltyPortion = penaltyPortion.subtract(totalAccPenalty).subtract(totalCreditedPenalty); - amount = amount.add(penaltyPortion); - totalAccPenalty = totalAccPenalty.add(penaltyPortion); - if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) { - penaltyPortion = null; - } - } - - if (amount.compareTo(BigDecimal.ZERO) > 0) { - addAccrualAccounting(accrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, penaltyPortion, - totalAccPenalty, tillDate); - } + BigDecimal newAccruedPenalty = MathUtil.zeroToNull(accrualData.getDueDatePenaltyIncome()); + BigDecimal penaltyPortion = MathUtil + .zeroToNull(MathUtil.subtract(newAccruedPenalty, accrualData.getAccruedPenaltyIncome(), accrualData.getCreditedPenalty())); + + LocalDate accruedTill = ACCRUAL_ON_CHARGE_DUE_DATE.equalsIgnoreCase(configurationDomainService.getAccrualDateConfigForCharge()) + ? accrualData.getDueDateAsLocaldate() + : DateUtils.getBusinessLocalDate(); + addAccrualAccounting(loan, accrualData, interestPortion, newAccruedInterest, feePortion, newAccruedFee, penaltyPortion, + newAccruedPenalty, accruedTill); } - private BigDecimal getInterestAccruedTillDate(LocalDate tillDate, LoanScheduleAccrualData accrualData) { - BigDecimal interestPortion; - LocalDate interestStartDate = accrualData.getFromDateAsLocaldate(); - if (DateUtils.isBefore(accrualData.getFromDateAsLocaldate(), accrualData.getInterestCalculatedFrom())) { - if (DateUtils.isBefore(accrualData.getInterestCalculatedFrom(), accrualData.getDueDateAsLocaldate())) { - interestStartDate = accrualData.getInterestCalculatedFrom(); - } else { - interestStartDate = accrualData.getDueDateAsLocaldate(); - } - } - - int totalNumberOfDays = DateUtils.getExactDifferenceInDays(interestStartDate, accrualData.getDueDateAsLocaldate()); - LocalDate startDate = accrualData.getFromDateAsLocaldate(); - if (DateUtils.isBefore(startDate, accrualData.getInterestCalculatedFrom())) { - if (DateUtils.isBefore(accrualData.getInterestCalculatedFrom(), tillDate)) { - startDate = accrualData.getInterestCalculatedFrom(); - } else { - startDate = tillDate; + private void addAccrualAccounting(@NotNull Loan loan, LoanScheduleAccrualData accrualData, BigDecimal interestPortion, + BigDecimal newAccruedInterest, BigDecimal feePortion, BigDecimal newAccruedFee, BigDecimal penaltyPortion, + BigDecimal newAccruedPenalty, final LocalDate accruedTill) throws DataAccessException { + AppUser user = context.authenticatedUser(); + Office office = officeRepository.getReferenceById(accrualData.getOfficeId()); + MonetaryCurrency currency = loan.getCurrency(); + LoanTransaction adjustTransaction = null; + if (isProgressiveAccrual(loan)) { + BigDecimal interestAdjustment = MathUtil.negativeToZero(MathUtil.negate(interestPortion)); + interestPortion = MathUtil.negativeToZero(interestPortion); + BigDecimal feeAdjustment = MathUtil.negativeToZero(MathUtil.negate(feePortion)); + feePortion = MathUtil.negativeToZero(feePortion); + BigDecimal penaltyAdjustment = MathUtil.negativeToZero(MathUtil.negate(penaltyPortion)); + penaltyPortion = MathUtil.negativeToZero(penaltyPortion); + BigDecimal totalAdjustment = MathUtil.add(interestAdjustment, feeAdjustment, penaltyAdjustment); + if (MathUtil.isGreaterThanZero(totalAdjustment)) { + adjustTransaction = accrualAdjustment(loan, office, accruedTill, totalAdjustment, interestAdjustment, feeAdjustment, + penaltyAdjustment, externalIdFactory.create()); } } - int daysToBeAccrued = DateUtils.getExactDifferenceInDays(startDate, tillDate); - double interestPerDay = accrualData.getAccruableIncome().doubleValue() / totalNumberOfDays; - if (daysToBeAccrued >= totalNumberOfDays) { - interestPortion = accrualData.getAccruableIncome(); - } else { - interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued); + // create accrual Transaction + LoanTransaction accrualTransaction = null; + BigDecimal amount = MathUtil.add(interestPortion, feePortion, penaltyPortion); + if (MathUtil.isGreaterThanZero(amount)) { + accrualTransaction = accrueTransaction(loan, office, accruedTill, amount, interestPortion, feePortion, penaltyPortion, + externalIdFactory.create()); } - interestPortion = interestPortion.setScale(accrualData.getCurrencyData().getDecimalPlaces(), MoneyHelper.getRoundingMode()); - return interestPortion; - } - - private void calculateFinalAccrualsForScheduleAndAddAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) { - - BigDecimal amount = BigDecimal.ZERO; - BigDecimal interestPortion = null; - BigDecimal totalAccInterest = null; - // interest - if (scheduleAccrualData.getAccruableIncome() != null) { - interestPortion = scheduleAccrualData.getAccruableIncome(); - totalAccInterest = interestPortion; - if (scheduleAccrualData.getAccruedInterestIncome() != null) { - interestPortion = interestPortion.subtract(scheduleAccrualData.getAccruedInterestIncome()); - } - amount = amount.add(interestPortion); - if (interestPortion.compareTo(BigDecimal.ZERO) == 0) { - interestPortion = null; - } + if (adjustTransaction == null && accrualTransaction == null) { + return; } - // fee - BigDecimal feePortion = null; - BigDecimal totalAccFee = null; - if (scheduleAccrualData.getDueDateFeeIncome() != null) { - feePortion = scheduleAccrualData.getDueDateFeeIncome(); - totalAccFee = feePortion; - if (scheduleAccrualData.getAccruedFeeIncome() != null) { - feePortion = feePortion.subtract(scheduleAccrualData.getAccruedFeeIncome()); - } - if (scheduleAccrualData.getCreditedFee() != null) { - feePortion = feePortion.subtract(scheduleAccrualData.getCreditedFee()); - } - amount = amount.add(feePortion); - if (feePortion.compareTo(BigDecimal.ZERO) == 0) { - feePortion = null; - } - } + // update repayment schedule portions + LoanRepaymentScheduleInstallment loanScheduleInstallment = loan + .fetchLoanRepaymentScheduleInstallmentByDueDate(accrualData.getDueDate()); + loanScheduleInstallment.updateAccrualPortion(Money.of(currency, newAccruedInterest), Money.of(currency, newAccruedFee), + Money.of(currency, newAccruedPenalty)); + // update loan accrued till date + loan.setAccruedTill(accruedTill); + loan.setLastModifiedBy(user.getId()); + loan.setLastModifiedDate(DateUtils.getAuditOffsetDateTime()); - // penalty - BigDecimal penaltyPortion = null; - BigDecimal totalAccPenalty = null; - if (scheduleAccrualData.getDueDatePenaltyIncome() != null) { - penaltyPortion = scheduleAccrualData.getDueDatePenaltyIncome(); - totalAccPenalty = penaltyPortion; - if (scheduleAccrualData.getAccruedPenaltyIncome() != null) { - penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getAccruedPenaltyIncome()); - } - if (scheduleAccrualData.getCreditedPenalty() != null) { - penaltyPortion = penaltyPortion.subtract(scheduleAccrualData.getCreditedPenalty()); + // update charges paid by + Integer installmentNumber = accrualData.getInstallmentNumber(); + Map applicableCharges = accrualData.getApplicableCharges(); + for (Map.Entry entry : applicableCharges.entrySet()) { + LoanCharge loanCharge = loanChargeRepository.getReferenceById(entry.getKey().getId()); + if (adjustTransaction != null) { + adjustTransaction.getLoanChargesPaid() + .add(new LoanChargePaidBy(adjustTransaction, loanCharge, entry.getValue(), installmentNumber)); } - amount = amount.add(penaltyPortion); - if (penaltyPortion.compareTo(BigDecimal.ZERO) == 0) { - penaltyPortion = null; + if (accrualTransaction != null) { + accrualTransaction.getLoanChargesPaid() + .add(new LoanChargePaidBy(accrualTransaction, loanCharge, entry.getValue(), installmentNumber)); } } - if (amount.compareTo(BigDecimal.ZERO) > 0) { - final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge(); - if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) { - addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, - penaltyPortion, totalAccPenalty, scheduleAccrualData.getDueDateAsLocaldate()); - } else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) { - addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, - penaltyPortion, totalAccPenalty, DateUtils.getBusinessLocalDate()); - } + ArrayList> newLoanTransactions = new ArrayList<>(2); + if (adjustTransaction != null) { + loanTransactionRepository.save(adjustTransaction); + loan.addLoanTransaction(adjustTransaction); + businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(adjustTransaction)); + newLoanTransactions.add(adjustTransaction.toMapData(currency.getCode())); } - } - - private void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData, BigDecimal amount, BigDecimal interestPortion, - BigDecimal totalAccInterest, BigDecimal feePortion, BigDecimal totalAccFee, BigDecimal penaltyPortion, - BigDecimal totalAccPenalty, final LocalDate accruedTill) throws DataAccessException { - - AppUser user = context.authenticatedUser(); - Loan loan = loanRepository.getReferenceById(scheduleAccrualData.getLoanId()); - Office office = officeRepository.getReferenceById(scheduleAccrualData.getOfficeId()); - MonetaryCurrency currency = loan.getCurrency(); - - // create accrual Transaction - LoanTransaction loanTransaction = accrueTransaction(loan, office, accruedTill, amount, interestPortion, feePortion, penaltyPortion, - externalIdFactory.create()); - - // update charges paid by - Map applicableCharges = scheduleAccrualData.getApplicableCharges(); - - for (Map.Entry entry : applicableCharges.entrySet()) { - LoanChargeData chargeData = entry.getKey(); - // - LoanCharge loanCharge = loanChargeRepository.getReferenceById(chargeData.getId()); - loanTransaction.getLoanChargesPaid() - .add(new LoanChargePaidBy(loanTransaction, loanCharge, entry.getValue(), scheduleAccrualData.getInstallmentNumber())); - + if (accrualTransaction != null) { + loanTransactionRepository.save(accrualTransaction); + loan.addLoanTransaction(accrualTransaction); + businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction)); + newLoanTransactions.add(accrualTransaction.toMapData(currency.getCode())); } - loanTransactionRepository.saveAndFlush(loanTransaction); - loan.addLoanTransaction(loanTransaction); - - Map transactionMap = toMapData(loanTransaction.getId(), amount, interestPortion, feePortion, penaltyPortion, - scheduleAccrualData, accruedTill); - - // update repayment schedule portions - - LoanRepaymentScheduleInstallment loanScheduleInstallment = loan - .fetchLoanRepaymentScheduleInstallmentByDueDate(scheduleAccrualData.getDueDate()); - loanScheduleInstallment.updateAccrualPortion(Money.of(currency, totalAccInterest), Money.of(currency, totalAccFee), - Money.of(currency, totalAccPenalty)); - - // update loan accrued till date - loan.setAccruedTill(accruedTill); - loan.setLastModifiedBy(user.getId()); - loan.setLastModifiedDate(DateUtils.getAuditOffsetDateTime()); - loanRepository.saveAndFlush(loan); - businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(loanTransaction)); - - final Map accountingBridgeData = deriveAccountingBridgeData(scheduleAccrualData, transactionMap); + final Map accountingBridgeData = deriveAccountingBridgeData(accrualData, newLoanTransactions); this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } - private Map deriveAccountingBridgeData(final LoanScheduleAccrualData loanScheduleAccrualData, - final Map transactionMap) { - + private Map deriveAccountingBridgeData(@NotNull LoanScheduleAccrualData loanScheduleAccrualData, + @NotNull List> newLoanTransactions) { final Map accountingBridgeData = new LinkedHashMap<>(); accountingBridgeData.put("loanId", loanScheduleAccrualData.getLoanId()); accountingBridgeData.put("loanProductId", loanScheduleAccrualData.getLoanProductId()); @@ -561,65 +678,38 @@ private Map deriveAccountingBridgeData(final LoanScheduleAccrual accountingBridgeData.put("isChargeOff", false); accountingBridgeData.put("isFraud", false); - final List> newLoanTransactions = new ArrayList<>(); - newLoanTransactions.add(transactionMap); - accountingBridgeData.put("newLoanTransactions", newLoanTransactions); return accountingBridgeData; } - public Map toMapData(final Long id, final BigDecimal amount, final BigDecimal interestPortion, - final BigDecimal feePortion, final BigDecimal penaltyPortion, final LoanScheduleAccrualData loanScheduleAccrualData, - final LocalDate accruedTill) { - final Map thisTransactionData = new LinkedHashMap<>(); - - final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.ACCRUAL); - - thisTransactionData.put("id", id); - thisTransactionData.put("officeId", loanScheduleAccrualData.getOfficeId()); - thisTransactionData.put("type", transactionType); - thisTransactionData.put("reversed", false); - thisTransactionData.put("date", accruedTill); - thisTransactionData.put("currency", loanScheduleAccrualData.getCurrencyData()); - thisTransactionData.put("amount", amount); - thisTransactionData.put("principalPortion", null); - thisTransactionData.put("interestPortion", interestPortion); - thisTransactionData.put("feeChargesPortion", feePortion); - thisTransactionData.put("penaltyChargesPortion", penaltyPortion); - thisTransactionData.put("overPaymentPortion", null); - - Map applicableCharges = loanScheduleAccrualData.getApplicableCharges(); - if (applicableCharges != null && !applicableCharges.isEmpty()) { - final List> loanChargesPaidData = new ArrayList<>(); - for (Map.Entry entry : applicableCharges.entrySet()) { - LoanChargeData chargeData = entry.getKey(); - final Map loanChargePaidData = new LinkedHashMap<>(); - loanChargePaidData.put("chargeId", chargeData.getChargeId()); - loanChargePaidData.put("isPenalty", chargeData.isPenalty()); - loanChargePaidData.put("loanChargeId", chargeData.getId()); - loanChargePaidData.put("amount", entry.getValue()); - - loanChargesPaidData.add(loanChargePaidData); - } - thisTransactionData.put("loanChargesPaid", loanChargesPaidData); - } - - return thisTransactionData; + private Map deriveAccountingBridgeData(@NotNull Loan loan, List> newLoanTransactions) { + final Map accountingBridgeData = new LinkedHashMap<>(); + accountingBridgeData.put("loanId", loan.getId()); + accountingBridgeData.put("loanProductId", loan.getLoanProduct().getId()); + accountingBridgeData.put("officeId", loan.getOfficeId()); + accountingBridgeData.put("currencyCode", loan.getCurrencyCode()); + accountingBridgeData.put("cashBasedAccountingEnabled", loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()); + accountingBridgeData.put("upfrontAccrualBasedAccountingEnabled", loan.isUpfrontAccrualAccountingEnabledOnLoanProduct()); + accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()); + accountingBridgeData.put("isAccountTransfer", false); + accountingBridgeData.put("isChargeOff", false); + accountingBridgeData.put("isFraud", false); + accountingBridgeData.put("newLoanTransactions", newLoanTransactions); + return accountingBridgeData; } - private void updateCharges(final Collection chargesData, final LoanScheduleAccrualData accrualData, - final LocalDate startDate, final LocalDate endDate) { + private void updateCharges(final List chargesData, final LoanScheduleAccrualData accrualData, final LocalDate startDate, + final LocalDate endDate) { final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge(); if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) { updateChargeForDueDate(chargesData, accrualData, startDate, endDate); } else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) { updateChargeForSubmittedOnDate(chargesData, accrualData, startDate, endDate); } - } - private void updateChargeForSubmittedOnDate(Collection chargesData, LoanScheduleAccrualData accrualData, - LocalDate startDate, LocalDate endDate) { + private void updateChargeForSubmittedOnDate(List chargesData, LoanScheduleAccrualData accrualData, LocalDate startDate, + LocalDate endDate) { final Map applicableCharges = new HashMap<>(); BigDecimal submittedDateFeeIncome = BigDecimal.ZERO; BigDecimal submittedDatePenaltyIncome = BigDecimal.ZERO; @@ -657,7 +747,7 @@ private boolean isChargeSubmittedDateAndDueDateInRange(LoanScheduleAccrualData a && !DateUtils.isBefore(scheduleEndDate, loanCharge.getDueDate()); } - private void updateChargeForDueDate(Collection chargesData, LoanScheduleAccrualData accrualData, LocalDate startDate, + private void updateChargeForDueDate(List chargesData, LoanScheduleAccrualData accrualData, LocalDate startDate, LocalDate endDate) { final Map applicableCharges = new HashMap<>(); BigDecimal dueDateFeeIncome = BigDecimal.ZERO; @@ -719,7 +809,6 @@ private BigDecimal calculateInstallmentFeeCharges(LoanScheduleAccrualData accrua BigDecimal installmentFeeChargeAmount = chargeAmount; Collection installmentData = loanCharge.getInstallmentChargeData(); for (LoanInstallmentChargeData installmentChargeData : installmentData) { - if (installmentChargeData.getInstallmentNumber().equals(accrualData.getInstallmentNumber())) { BigDecimal accruableForInstallment = installmentChargeData.getAmount(); if (installmentChargeData.getAmountUnrecognized() != null) { @@ -746,16 +835,13 @@ private BigDecimal calculateInstallmentFeeCharges(LoanScheduleAccrualData accrua return installmentFeeChargeAmount; } - private void updateInterestIncome(final LoanScheduleAccrualData accrualData, - final Collection loanWaiverTransactions, - final Collection loanSchedulePeriodDataList, final LocalDate tillDate) { - - BigDecimal interestIncome = BigDecimal.ZERO; - if (accrualData.getInterestIncome() != null) { - interestIncome = accrualData.getInterestIncome(); - } + private void updateInterestIncome(Loan loan, @NotNull LoanScheduleAccrualData accrualData, + @NotNull List loanWaiverTransactions, @NotNull List loanSchedulePeriodDataList, + @NotNull LocalDate tillDate) { + BigDecimal accruableIncome = accrualData.getAccruableIncome() == null ? accrualData.getInterestIncome() + : accrualData.getAccruableIncome(); if (accrualData.getWaivedInterestIncome() != null) { - Collection loanTransactionDatas = new ArrayList<>(); + List loanTransactionDatas = new ArrayList<>(); getLoanWaiverTransactionsInRange(accrualData, loanWaiverTransactions, tillDate, loanTransactionDatas); @@ -763,28 +849,28 @@ private void updateInterestIncome(final LoanScheduleAccrualData accrualData, BigDecimal interestWaived = accrualData.getWaivedInterestIncome(); if (interestWaived.compareTo(recognized) > 0) { - interestIncome = interestIncome.subtract(interestWaived.subtract(recognized)); + accruableIncome = accruableIncome.subtract(interestWaived.subtract(recognized)); } } - accrualData.updateAccruableIncome(interestIncome); + accrualData.updateAccruableIncome(accruableIncome); } - private BigDecimal getWaivedInterestIncome(LoanScheduleAccrualData accrualData, - Collection loanSchedulePeriodDataList, Collection loanTransactionDatas) { + private BigDecimal getWaivedInterestIncome(LoanScheduleAccrualData accrualData, List waivedPeriodDataList, + List loanTransactionDatas) { BigDecimal recognized = BigDecimal.ZERO; BigDecimal unrecognized = BigDecimal.ZERO; BigDecimal remainingAmt = BigDecimal.ZERO; Iterator iterator = loanTransactionDatas.iterator(); - for (LoanSchedulePeriodData loanSchedulePeriodData : loanSchedulePeriodDataList) { + for (LoanSchedulePeriodData waivedPeriodData : waivedPeriodDataList) { if (MathUtil.isLessThanOrEqualZero(recognized) && MathUtil.isLessThanOrEqualZero(unrecognized) && iterator.hasNext()) { LoanTransactionData loanTransactionData = iterator.next(); recognized = recognized.add(loanTransactionData.getInterestPortion()); unrecognized = unrecognized.add(loanTransactionData.getUnrecognizedIncomePortion()); } - if (DateUtils.isBefore(loanSchedulePeriodData.getDueDate(), accrualData.getDueDateAsLocaldate())) { - remainingAmt = remainingAmt.add(loanSchedulePeriodData.getInterestWaived()); + if (DateUtils.isBefore(waivedPeriodData.getDueDate(), accrualData.getDueDateAsLocaldate())) { + remainingAmt = remainingAmt.add(waivedPeriodData.getInterestWaived()); if (recognized.compareTo(remainingAmt) > 0) { recognized = recognized.subtract(remainingAmt); remainingAmt = BigDecimal.ZERO; @@ -805,9 +891,8 @@ private BigDecimal getWaivedInterestIncome(LoanScheduleAccrualData accrualData, return recognized; } - private void getLoanWaiverTransactionsInRange(LoanScheduleAccrualData accrualData, - Collection loanWaiverTransactions, LocalDate tillDate, - Collection loanTransactionDatas) { + private void getLoanWaiverTransactionsInRange(LoanScheduleAccrualData accrualData, List loanWaiverTransactions, + LocalDate tillDate, List loanTransactionDatas) { for (LoanTransactionData loanTransactionData : loanWaiverTransactions) { LocalDate transactionDate = loanTransactionData.getDate(); if (!DateUtils.isAfter(transactionDate, accrualData.getFromDateAsLocaldate()) @@ -828,21 +913,22 @@ private void postJournalEntries(final Loan loan, final List existingTransa journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } - private void reprocessPeriodicAccruals(Loan loan, final Collection accruals) { - if (!loan.isChargedOff()) { - List installments = loan.getRepaymentScheduleInstallments(); - boolean isBasedOnSubmittedOnDate = configurationDomainService.getAccrualDateConfigForCharge() - .equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE); - for (LoanRepaymentScheduleInstallment installment : installments) { - checkAndUpdateAccrualsForInstallment(loan, accruals, installments, isBasedOnSubmittedOnDate, installment); - } - // reverse accruals after last installment - LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment(); - reverseTransactionsPostEffectiveDate(accruals, lastInstallment.getDueDate()); + private void reprocessPeriodicAccruals(Loan loan, final List accruals) { + if (loan.isChargedOff() || isProgressiveAccrual(loan)) { + return; + } + List installments = loan.getRepaymentScheduleInstallments(); + boolean isBasedOnSubmittedOnDate = configurationDomainService.getAccrualDateConfigForCharge() + .equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE); + for (LoanRepaymentScheduleInstallment installment : installments) { + checkAndUpdateAccrualsForInstallment(loan, accruals, installments, isBasedOnSubmittedOnDate, installment); } + // reverse accruals after last installment + LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment(); + reverseTransactionsPostEffectiveDate(accruals, lastInstallment.getDueDate()); } - private void checkAndUpdateAccrualsForInstallment(Loan loan, Collection accruals, + private void checkAndUpdateAccrualsForInstallment(Loan loan, List accruals, List installments, boolean isBasedOnSubmittedOnDate, LoanRepaymentScheduleInstallment installment) { Money interest = Money.zero(loan.getCurrency()); @@ -884,7 +970,10 @@ private LocalDate getDateForRangeCalculation(LoanTransaction loanTransaction, bo : loanTransaction.getTransactionDate(); } - private void reprocessNonPeriodicAccruals(Loan loan, final Collection accruals) { + private void reprocessNonPeriodicAccruals(Loan loan, final List accruals) { + if (isProgressiveAccrual(loan)) { + return; + } final Money interestApplied = Money.of(loan.getCurrency(), loan.getSummary().getTotalInterestCharged()); ExternalId externalId = ExternalId.empty(); boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled(); @@ -914,82 +1003,8 @@ private void reprocessNonPeriodicAccruals(Loan loan, final Collection loanScheduleAccrualList) { - boolean isOrganisationDateEnabled = configurationDomainService.isOrganisationstartDateEnabled(); - LocalDate organisationStartDate = DateUtils.getBusinessLocalDate(); - if (isOrganisationDateEnabled) { - organisationStartDate = configurationDomainService.retrieveOrganisationStartDate(); - } - List installments = loan.getRepaymentScheduleInstallments(); - Long loanId = loan.getId(); - Long officeId = loan.getOfficeId(); - LocalDate accrualStartDate = null; - PeriodFrequencyType repaymentFrequency = loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType(); - Integer repayEvery = loan.repaymentScheduleDetail().getRepayEvery(); - LocalDate interestCalculatedFrom = loan.getInterestChargedFromDate(); - Long loanProductId = loan.productId(); - MonetaryCurrency currency = loan.getCurrency(); - ApplicationCurrency applicationCurrency = applicationCurrencyRepository.findOneWithNotFoundDetection(currency); - CurrencyData currencyData = applicationCurrency.toData(); - Set loanCharges = loan.getActiveCharges(); - int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); - - for (LoanRepaymentScheduleInstallment installment : installments) { - if (DateUtils.isAfter(installment.getDueDate(), loan.getMaturityDate())) { - accruedTill = DateUtils.getBusinessLocalDate(); - } - if (!isOrganisationDateEnabled || DateUtils.isBefore(organisationStartDate, installment.getDueDate())) { - boolean isFirstNormalInstallment = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber); - generateLoanScheduleAccrualData(accruedTill, loanScheduleAccrualList, loanId, officeId, accrualStartDate, - repaymentFrequency, repayEvery, interestCalculatedFrom, loanProductId, currency, currencyData, loanCharges, - installment, isFirstNormalInstallment); - } - } - return accruedTill; - } - - private void generateLoanScheduleAccrualData(final LocalDate accruedTill, - final Collection loanScheduleAccrualDatas, final Long loanId, Long officeId, - final LocalDate accrualStartDate, final PeriodFrequencyType repaymentFrequency, final Integer repayEvery, - final LocalDate interestCalculatedFrom, final Long loanProductId, final MonetaryCurrency currency, - final CurrencyData currencyData, final Set loanCharges, final LoanRepaymentScheduleInstallment installment, - boolean isFirstNormalInstallment) { - - if (!DateUtils.isBefore(accruedTill, installment.getDueDate()) || (DateUtils.isAfter(accruedTill, installment.getFromDate()) - && !DateUtils.isAfter(accruedTill, installment.getDueDate()))) { - BigDecimal dueDateFeeIncome = BigDecimal.ZERO; - BigDecimal dueDatePenaltyIncome = BigDecimal.ZERO; - LocalDate chargesTillDate = installment.getDueDate(); - if (!DateUtils.isAfter(accruedTill, installment.getDueDate())) { - chargesTillDate = accruedTill; - } - - for (final LoanCharge loanCharge : loanCharges) { - boolean isDue = loanCharge.isDueInPeriod(installment.getFromDate(), chargesTillDate, isFirstNormalInstallment); - if (isDue) { - if (loanCharge.isFeeCharge()) { - dueDateFeeIncome = dueDateFeeIncome.add(loanCharge.amount()); - } else if (loanCharge.isPenaltyCharge()) { - dueDatePenaltyIncome = dueDatePenaltyIncome.add(loanCharge.amount()); - } - } - } - LoanScheduleAccrualData accrualData = new LoanScheduleAccrualData(loanId, officeId, installment.getInstallmentNumber(), - accrualStartDate, repaymentFrequency, repayEvery, installment.getDueDate(), installment.getFromDate(), - installment.getId(), loanProductId, installment.getInterestCharged(currency).getAmount(), - installment.getFeeChargesCharged(currency).getAmount(), installment.getPenaltyChargesCharged(currency).getAmount(), - installment.getInterestAccrued(currency).getAmount(), installment.getFeeAccrued(currency).getAmount(), - installment.getPenaltyAccrued(currency).getAmount(), currencyData, interestCalculatedFrom, - installment.getInterestWaived(currency).getAmount(), installment.getCreditedFee(currency).getAmount(), - installment.getCreditedPenalty(currency).getAmount()); - loanScheduleAccrualDatas.add(accrualData); - - } - } - private void createAccrualTransactionAndUpdateChargesPaidBy(Loan loan, LocalDate foreClosureDate, - Collection newAccrualTransactions, MonetaryCurrency currency, Money interestPortion, Money feePortion, + List newAccrualTransactions, MonetaryCurrency currency, Money interestPortion, Money feePortion, Money penaltyPortion, Money total) { ExternalId accrualExternalId = externalIdFactory.create(); LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), foreClosureDate, total.getAmount(), @@ -1024,7 +1039,7 @@ private void determineReceivableIncomeForeClosure(Loan loan, final LocalDate til receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency)); receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency)); receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(currency)); - } else if (transaction.isRepaymentLikeType() || transaction.isChargePayment()) { + } else if (transaction.isRepaymentLikeType() || transaction.isChargePayment() || transaction.isAccrualAdjustment()) { receivableInterest = receivableInterest.minus(transaction.getInterestPortion(currency)); receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency)); receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency)); @@ -1047,7 +1062,8 @@ private void determineReceivableIncomeForeClosure(Loan loan, final LocalDate til } private List retrieveListOfAccrualTransactions(Loan loan) { - return loan.getLoanTransactions().stream().filter(transaction -> transaction.isNotReversed() && transaction.isAccrual()) + return loan.getLoanTransactions().stream() + .filter(transaction -> transaction.isNotReversed() && (transaction.isAccrual() || transaction.isAccrualAdjustment())) .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); } @@ -1122,13 +1138,11 @@ private void createUpdateAccrualTransaction(Loan loan, LoanInterestRecalcualtion } if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - if (existingAccrualTransaction == null) { - LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), - compoundingDetail.getAmount(), interest, fee, penalties, externalId); - updateLoanChargesPaidBy(loan, accrual, feeDetails, null); - loan.addLoanTransaction(accrual); - } else if (existingAccrualTransaction.getAmount(loan.getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) { - existingAccrualTransaction.reverse(); + if (existingAccrualTransaction == null + || !MathUtil.isEqualTo(existingAccrualTransaction.getAmount(), compoundingDetail.getAmount())) { + if (existingAccrualTransaction != null) { + existingAccrualTransaction.reverse(); + } LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), compoundingDetail.getAmount(), interest, fee, penalties, externalId); updateLoanChargesPaidBy(loan, accrual, feeDetails, null); @@ -1217,7 +1231,7 @@ private void updateLoanChargesPaidBy(Loan loan, LoanTransaction accrual, Map transactions, LocalDate effectiveDate) { + private void reverseTransactionsPostEffectiveDate(List transactions, LocalDate effectiveDate) { for (LoanTransaction loanTransaction : transactions) { if (DateUtils.isAfter(loanTransaction.getTransactionDate(), effectiveDate)) { loanTransaction.reverse(); @@ -1277,12 +1291,13 @@ private void updateLoanChargesAndInstallmentChargesPaidBy(Loan loan, LoanTransac Map accrualDetails = loan.getActiveCharges().stream() .collect(Collectors.toMap(LoanCharge::getId, v -> Money.zero(currency))); - loan.getLoanTransactions(LoanTransaction::isAccrual).forEach(transaction -> { - transaction.getLoanChargesPaid().forEach(loanChargePaid -> { - accrualDetails.computeIfPresent(loanChargePaid.getLoanCharge().getId(), - (mappedKey, mappedValue) -> mappedValue.add(Money.of(currency, loanChargePaid.getAmount()))); - }); - }); + loan.getLoanTransactions(e -> !e.isReversed() && (e.isAccrual() || e.isAccrualAdjustment())) + .forEach(transaction -> transaction.getLoanChargesPaid().forEach(loanChargePaid -> { + accrualDetails.computeIfPresent(loanChargePaid.getLoanCharge().getId(), (mappedKey, mappedValue) -> { + Money amount = Money.of(currency, loanChargePaid.getAmount()); + return transaction.isAccrual() ? mappedValue.add(amount) : mappedValue.minus(amount); + }); + })); loan.getActiveCharges().forEach(loanCharge -> { Money amount = loanCharge.getAmount(currency).minus(loanCharge.getAmountWaived(currency)); @@ -1321,9 +1336,8 @@ private void updateLoanChargesAndInstallmentChargesPaidBy(Loan loan, LoanTransac private LoanTransaction createAccrualTransaction(Loan loan, Money interestPortion, Money feePortion, Money penaltyPortion, Money total, LocalDate accrualTransactionDate) { ExternalId externalId = externalIdFactory.create(); - LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), accrualTransactionDate, - total.getAmount(), interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(), externalId); - return accrualTransaction; + return LoanTransaction.accrueTransaction(loan, loan.getOffice(), accrualTransactionDate, total.getAmount(), + interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(), externalId); } private void determineReceivableIncomeDetailsForLoanClosure(Loan loan, Map incomeDetails) { @@ -1339,21 +1353,21 @@ private void determineReceivableIncomeDetailsForLoanClosure(Loan loan, Map chargeTransactions = loan + .getLoanTransactions(t -> t.isAccrual() || t.isAccrualAdjustment() || t.isChargesWaiver()); + for (LoanCharge loanCharge : loan.getActiveCharges()) { BigDecimal accruedAmount = BigDecimal.ZERO; BigDecimal waivedAmount = BigDecimal.ZERO; - for (LoanTransaction loanTransaction : loan.getLoanTransactions()) { - if (loanTransaction.isAccrual() || loanTransaction.isChargesWaiver()) { - for (LoanChargePaidBy loanChargePaidBy : loanTransaction.getLoanChargesPaid()) { - if (loanChargePaidBy.getLoanCharge().getId().equals(loanCharge.getId())) { - if (loanTransaction.isAccrual()) { - accruedAmount = accruedAmount.add(loanTransaction.getAmount()); - } else if (loanTransaction.isChargesWaiver()) { - waivedAmount = waivedAmount.add(loanTransaction.getAmount()); - } + for (LoanTransaction chargeTransaction : chargeTransactions) { + for (LoanChargePaidBy loanChargePaidBy : chargeTransaction.getLoanChargesPaid()) { + if (loanChargePaidBy.getLoanCharge().getId().equals(loanCharge.getId())) { + BigDecimal amount = chargeTransaction.getAmount(); + if (chargeTransaction.isAccrual()) { + accruedAmount = accruedAmount.add(amount); + } else if (chargeTransaction.isAccrualAdjustment()) { + waivedAmount = accruedAmount.subtract(amount); + } else if (chargeTransaction.isChargesWaiver()) { + waivedAmount = waivedAmount.add(amount); } } } @@ -1369,10 +1383,10 @@ private void determineReceivableIncomeDetailsForLoanClosure(Loan loan, Map transactions, + private void determineCumulativeIncomeDetails(Loan loan, List transactions, HashMap incomeDetailsMap) { BigDecimal interest = BigDecimal.ZERO; BigDecimal fee = BigDecimal.ZERO; @@ -1474,4 +1488,7 @@ private LocalDate getFinalAccrualTransactionDate(Loan loan) { }; } + public static boolean isProgressiveAccrual(@NotNull Loan loan) { + return loan.getLoanProductRelatedDetail().getLoanScheduleType() == LoanScheduleType.PROGRESSIVE; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java similarity index 99% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java index 34cf975ba93..214bb16d3d2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java @@ -104,7 +104,7 @@ import org.apache.fineract.useradministration.domain.AppUser; @RequiredArgsConstructor -public class LoanAssembler { +public class LoanAssemblerImpl implements LoanAssembler { private final FromJsonHelper fromApiJsonHelper; private final LoanRepositoryWrapper loanRepository; @@ -136,6 +136,7 @@ public class LoanAssembler { private final LoanCollateralManagementMapper loanCollateralManagementMapper; private final LoanAccrualsProcessingService loanAccrualsProcessingService; + @Override public Loan assembleFrom(final Long accountId) { final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true); loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, @@ -144,11 +145,13 @@ public Loan assembleFrom(final Long accountId) { return loanAccount; } + @Override public void setHelpers(final Loan loanAccount) { loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); } + @Override public Loan assembleFrom(final JsonCommand command) { final JsonElement element = command.parsedJson(); @@ -281,6 +284,7 @@ public Loan assembleFrom(final JsonCommand command) { // TODO: Review... it might be better somewhere else and rethink due to the account number generation logic is // intertwined with GLIM logic + @Override public void accountNumberGeneration(JsonCommand command, Loan loan) { if (loan.isAccountNumberRequiresAutoGeneration()) { JsonElement element = command.parsedJson(); @@ -371,6 +375,7 @@ private void topUpLoanConfiguration(JsonElement element, Loan loan) { } } + @Override public CodeValue findCodeValueByIdIfProvided(final Long codeValueId) { CodeValue codeValue = null; if (codeValueId != null) { @@ -379,6 +384,7 @@ public CodeValue findCodeValueByIdIfProvided(final Long codeValueId) { return codeValue; } + @Override public Fund findFundByIdIfProvided(final Long fundId) { Fund fund = null; if (fundId != null) { @@ -387,6 +393,7 @@ public Fund findFundByIdIfProvided(final Long fundId) { return fund; } + @Override public Staff findLoanOfficerByIdIfProvided(final Long loanOfficerId) { Staff staff = null; if (loanOfficerId != null) { @@ -419,6 +426,7 @@ private void copyAdvancedPaymentRulesIfApplicable(String transactionProcessingSt } } + @Override public Map updateFrom(JsonCommand command, Loan loan) { final Map changes = new HashMap<>(); LoanProduct loanProduct; @@ -844,6 +852,7 @@ public Map updateFrom(JsonCommand command, Loan loan) { return changes; } + @Override public Map updateLoanApplicationAttributesForWithdrawal(Loan loan, JsonCommand command, AppUser currentUser) { final Map actualChanges = new LinkedHashMap<>(); @@ -868,6 +877,7 @@ public Map updateLoanApplicationAttributesForWithdrawal(Loan loa return actualChanges; } + @Override public Map updateLoanApplicationAttributesForRejection(Loan loan, JsonCommand command, AppUser currentUser) { final Map actualChanges = new LinkedHashMap<>(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java index 2ebaaf1f824..14641ede856 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java @@ -214,7 +214,7 @@ public LoanChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") fin } @Override - public Collection retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges) { + public List retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges) { final LoanInstallmentChargeMapper rm = new LoanInstallmentChargeMapper(); String sql = "select " + rm.schema() + "where lic.loan_charge_id= ? "; if (onlyPaymentPendingCharges) { @@ -264,14 +264,14 @@ public Collection retrieveOverdueInstallmentChargeFrequencyNumber(final } @Override - public Collection retrieveLoanChargesForAccrual(final Long loanId) { + public List retrieveLoanChargesForAccrual(final Long loanId) { final LoanChargeAccrualMapper rm = new LoanChargeAccrualMapper(); final String sql = "select " + rm.schema() + " where lc.loan_id=? AND lc.is_active = true group by lc.id " + " order by lc.charge_time_enum ASC, lc.due_for_collection_as_of_date ASC, lc.is_penalty ASC"; - Collection charges = this.jdbcTemplate.query(sql, rm, // NOSONAR + List charges = this.jdbcTemplate.query(sql, rm, // NOSONAR LoanTransactionType.ACCRUAL.getValue(), loanId, loanId); charges = updateLoanChargesWithUnrecognizedIncome(loanId, charges); @@ -284,8 +284,7 @@ public Collection retrieveLoanChargesForAccrual(final Long loanI charges.removeAll(removeCharges); for (LoanChargeData loanChargeData : removeCharges) { if (loanChargeData.isInstallmentFee()) { - Collection installmentChargeDatas = retrieveInstallmentLoanChargesForAccrual( - loanChargeData.getId()); + List installmentChargeDatas = retrieveInstallmentLoanChargesForAccrual(loanChargeData.getId()); LoanChargeData modifiedChargeData = new LoanChargeData(loanChargeData, installmentChargeDatas); charges.add(modifiedChargeData); } @@ -347,8 +346,7 @@ public LoanChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") fin } } - private Collection updateLoanChargesWithUnrecognizedIncome(final Long loanId, - Collection loanChargeDatas) { + private List updateLoanChargesWithUnrecognizedIncome(final Long loanId, List loanChargeDatas) { final LoanChargeUnRecognizedIncomeMapper rm = new LoanChargeUnRecognizedIncomeMapper(loanChargeDatas); @@ -398,11 +396,11 @@ public LoanChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") fin } } - private Collection retrieveInstallmentLoanChargesForAccrual(Long loanChargeId) { + private List retrieveInstallmentLoanChargesForAccrual(Long loanChargeId) { final LoanInstallmentChargeAccrualMapper rm = new LoanInstallmentChargeAccrualMapper(); String sql = "select " + rm.schema() + " where lic.loan_charge_id= ? group by lsi.installment, lsi.duedate, lic.amount_outstanding_derived, lic.amount, lic.is_paid_derived, lic.amount_waived_derived, lic.waived"; - Collection chargeDatas = this.jdbcTemplate.query(sql, rm, // NOSONAR + List chargeDatas = this.jdbcTemplate.query(sql, rm, // NOSONAR LoanTransactionType.ACCRUAL.getValue(), loanChargeId); final Map installmentChargeDatas = new HashMap<>(); for (LoanInstallmentChargeData installmentChargeData : chargeDatas) { @@ -412,8 +410,7 @@ private Collection retrieveInstallmentLoanChargesForA for (LoanInstallmentChargeData installmentChargeData : chargeDatas) { installmentChargeDatas.put(installmentChargeData.getInstallmentNumber(), installmentChargeData); } - return installmentChargeDatas.values(); - + return new ArrayList<>(installmentChargeDatas.values()); } private static final class LoanInstallmentChargeAccrualMapper implements RowMapper { @@ -461,7 +458,7 @@ public LoanInstallmentChargeData mapRow(final ResultSet rs, @SuppressWarnings("u } } - private Collection updateInstallmentLoanChargesWithUnrecognizedIncome(final Long loanChargeId, + private List updateInstallmentLoanChargesWithUnrecognizedIncome(final Long loanChargeId, final Map installmentChargeDatas) { final LoanInstallmentChargeUnRecognizedIncomeMapper rm = new LoanInstallmentChargeUnRecognizedIncomeMapper(installmentChargeDatas); String sql = "select " + rm.schema() + " where cpb.loan_charge_id = ? group by cpb.installment_number "; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java index 8f39c262e02..5198ac80a75 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -104,9 +104,9 @@ LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelated LoanTransactionData retrieveLoanWriteoffTemplate(Long loanId); - Collection retrievePeriodicAccrualData(LocalDate tillDate); + List retrievePeriodicAccrualData(LocalDate tillDate); - Collection retrievePeriodicAccrualData(LocalDate tillDate, Loan loan); + List retrievePeriodicAccrualData(LocalDate tillDate, Loan loan); LoanTransactionData retrieveLoanChargeOffTemplate(Long loanId); @@ -116,9 +116,9 @@ LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelated LoanTransactionData retrieveLoanPrePaymentTemplate(LoanTransactionType repaymentTransactionType, Long loanId, LocalDate onDate); - Collection retrieveWaiverLoanTransactions(Long loanId); + List retrieveWaiverLoanTransactions(Long loanId); - Collection fetchWaiverInterestRepaymentData(Long loanId); + List fetchWaiverInterestRepaymentData(Long loanId); boolean isGuaranteeRequired(Long loanId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 652fe67f03a..36bd4442e9e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -1795,12 +1795,12 @@ private Collection retrieveScheduleAccrualDataForCharge } @Override - public Collection retrievePeriodicAccrualData(final LocalDate tillDate) { + public List retrievePeriodicAccrualData(final LocalDate tillDate) { return retrievePeriodicAccrualData(tillDate, null); } @Override - public Collection retrievePeriodicAccrualData(final LocalDate tillDate, final Loan loan) { + public List retrievePeriodicAccrualData(final LocalDate tillDate, final Loan loan) { final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge(); if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) { return retrievePeriodicAccrualDataForChargeSubmittedDateProcessing(tillDate, loan); @@ -1808,7 +1808,7 @@ public Collection retrievePeriodicAccrualData(final Loc return retrievePeriodicAccrualDataForDefaultProcessing(tillDate, loan); } - private Collection retrievePeriodicAccrualDataForDefaultProcessing(final LocalDate tillDate, final Loan loan) { + private List retrievePeriodicAccrualDataForDefaultProcessing(final LocalDate tillDate, final Loan loan) { LoanSchedulePeriodicAccrualMapper mapper = new LoanSchedulePeriodicAccrualMapper(); LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate(); final StringBuilder sqlBuilder = new StringBuilder(400); @@ -1836,7 +1836,7 @@ private Collection retrievePeriodicAccrualDataForDefaul return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper); } - private Collection retrievePeriodicAccrualDataForChargeSubmittedDateProcessing(final LocalDate tillDate, + private List retrievePeriodicAccrualDataForChargeSubmittedDateProcessing(final LocalDate tillDate, final Loan loan) { LoanSchedulePeriodicAccrualMapper mapper = new LoanSchedulePeriodicAccrualMapper(); LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate(); @@ -2130,7 +2130,7 @@ public List fetchLoansForInterestRecalculation(Integer pageSize, Long maxL } @Override - public Collection retrieveWaiverLoanTransactions(final Long loanId) { + public List retrieveWaiverLoanTransactions(final Long loanId) { try { final LoanTransactionDerivedComponentMapper rm = new LoanTransactionDerivedComponentMapper(sqlGenerator); @@ -2196,7 +2196,7 @@ public LoanTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused" } @Override - public Collection fetchWaiverInterestRepaymentData(final Long loanId) { + public List fetchWaiverInterestRepaymentData(final Long loanId) { try { final LoanRepaymentWaiverMapper rm = new LoanRepaymentWaiverMapper(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java index 62ec46dcef5..684dbe72f2a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java @@ -28,6 +28,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; @@ -61,7 +62,7 @@ public boolean canHandle(Loan loan) { private static void simulateRepaymentForDisbursements(LoanTransaction lt, final AtomicReference refundFinal, List collect) { collect.add(lt); - if (lt.getTypeOf().isDisbursement() && refundFinal.get().compareTo(BigDecimal.ZERO) > 0) { + if (lt.getTypeOf().isDisbursement() && MathUtil.isGreaterThanZero(refundFinal.get())) { if (lt.getAmount().compareTo(refundFinal.get()) <= 0) { collect.add( new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), REPAYMENT.getValue(), lt.getDateOf(), lt.getAmount(), @@ -77,8 +78,6 @@ private static void simulateRepaymentForDisbursements(LoanTransaction lt, final } private BigDecimal totalInterest(final Loan loan, BigDecimal refundAmount, LocalDate relatedRefundTransactionDate) { - final AtomicReference refundFinal = new AtomicReference<>(refundAmount); - BigDecimal payableInterest = BigDecimal.ZERO; if (loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isDisbursement)) { List transactionsToReprocess = new ArrayList<>(); @@ -86,18 +85,16 @@ private BigDecimal totalInterest(final Loan loan, BigDecimal refundAmount, Local .map(LoanSupportedInterestRefundTypes::getTransactionType).toList(); // add already interest refunded amounts to refund amount // it is necessary to avoid multi disbursed refund + final AtomicReference refundFinal = new AtomicReference<>(refundAmount); loan.getLoanTransactions().stream() // - .filter(lt -> !lt.isReversed()) // - .filter(lt -> interestRefundTypes.contains(lt.getTypeOf())) // + .filter(lt -> !lt.isReversed() && interestRefundTypes.contains(lt.getTypeOf())) // .forEach(t -> refundFinal.set(refundFinal.get().add(t.getAmount()))); // - loan.getLoanTransactions().stream() // - .filter(lt -> !lt.isReversed()) // - .filter(lt -> !lt.isAccrual() && !lt.isAccrualActivity() && !lt.isInterestRefund()) // - .filter(loanTransaction -> !interestRefundTypes.contains(loanTransaction.getTypeOf())) // + loan.getLoanTransactions(lt -> !lt.isReversed() && !lt.isAccrualRelated() && !lt.isInterestRefund() + && !interestRefundTypes.contains(lt.getTypeOf())) // .forEach(lt -> simulateRepaymentForDisbursements(lt, refundFinal, transactionsToReprocess)); // - List installmentsToReprocess = new ArrayList<>( - loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList()); + List installmentsToReprocess = loan + .getRepaymentScheduleInstallments(i -> !i.isReAged() && !i.isAdditional()); Pair reprocessResult = processor .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java index 47b820906d0..93d25e7d0bf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java @@ -20,6 +20,7 @@ import java.util.List; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor; @@ -104,8 +105,8 @@ public LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTra @Bean @Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class) - public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator) { - return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator); + public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator, + LoanRepositoryWrapper loanRepositoryWrapper) { + return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper); } - } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java index d045c091731..bd5e5d17d18 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java @@ -102,6 +102,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService; import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanAssemblerImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanCalculateRepaymentPastDueService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService; @@ -217,7 +218,7 @@ public LoanAssembler loanAssembler(FromJsonHelper fromApiJsonHelper, LoanReposit LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService, LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler, LoanChargeMapper loanChargeMapper, LoanCollateralManagementMapper loanCollateralManagementMapper, LoanAccrualsProcessingService loanAccrualsProcessingService) { - return new LoanAssembler(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository, + return new LoanAssemblerImpl(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository, fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler, loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService, workingDaysRepository, rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository, diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0099_add_accrual_transaction_external_event_configuration.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0099_add_accrual_transaction_external_event_configuration.xml index 9fffdcaecf6..46aa479d066 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0099_add_accrual_transaction_external_event_configuration.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0099_add_accrual_transaction_external_event_configuration.xml @@ -28,4 +28,10 @@ + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java index a62e1f2dcca..6b3d6054c99 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTest.java @@ -177,13 +177,20 @@ public void testGetLastUserTransaction() { final LoanTransaction loanTransaction = Mockito.mock(LoanTransaction.class); when(loanTransaction.isNotReversed()).thenReturn(Boolean.TRUE); - when(loanTransaction.isAccrualTransaction()).thenReturn(Boolean.FALSE); + when(loanTransaction.isAccrual()).thenReturn(Boolean.FALSE); + when(loanTransaction.isAccrualAdjustment()).thenReturn(Boolean.FALSE); final LoanTransaction loanTransaction2 = Mockito.mock(LoanTransaction.class); when(loanTransaction2.isNotReversed()).thenReturn(Boolean.TRUE); - when(loanTransaction2.isAccrualTransaction()).thenReturn(Boolean.FALSE); + when(loanTransaction2.isAccrual()).thenReturn(Boolean.FALSE); + when(loanTransaction2.isAccrualAdjustment()).thenReturn(Boolean.FALSE); final LoanTransaction loanTransaction3 = Mockito.mock(LoanTransaction.class); when(loanTransaction3.isNotReversed()).thenReturn(Boolean.TRUE); - when(loanTransaction3.isAccrualTransaction()).thenReturn(Boolean.TRUE); + when(loanTransaction3.isAccrual()).thenReturn(Boolean.TRUE); + when(loanTransaction3.isAccrualAdjustment()).thenReturn(Boolean.FALSE); + final LoanTransaction loanTransaction4 = Mockito.mock(LoanTransaction.class); + when(loanTransaction4.isNotReversed()).thenReturn(Boolean.TRUE); + when(loanTransaction4.isAccrual()).thenReturn(Boolean.FALSE); + when(loanTransaction4.isAccrualAdjustment()).thenReturn(Boolean.TRUE); ReflectionTestUtils.setField(loan, "loanTransactions", List.of(loanTransaction, loanTransaction2, loanTransaction3)); final LoanTransaction userTransaction = loan.getLastUserTransaction(); assertNotNull(userTransaction);