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 a54e32e5f..74ee83eb8 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 @@ -56,24 +56,18 @@ import lombok.Getter; import lombok.Setter; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.codes.domain.CodeValue; -import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator; -import org.apache.fineract.organisation.holiday.domain.Holiday; -import org.apache.fineract.organisation.holiday.service.HolidayUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.staff.domain.Staff; -import org.apache.fineract.organisation.workingdays.domain.WorkingDays; import org.apache.fineract.portfolio.accountdetails.domain.AccountType; -import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; -import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.collateral.domain.LoanCollateral; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; @@ -265,6 +259,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @JoinColumn(name = "rescheduledon_userid") private AppUser rescheduledByUser; + @Setter @Column(name = "expected_maturedon_date") private LocalDate expectedMaturityDate; @@ -628,65 +623,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return collateral; } - /** - * Creates a loanTransaction for "Apply Charge Event" with transaction date set to "suppliedTransactionDate". The - * newly created transaction is also added to the Loan on which this method is called. - * - * If "suppliedTransactionDate" is not passed Id, the transaction date is set to the loans due date if the due date - * is lesser than today's date. If not, the transaction date is set to today's date - */ - public LoanTransaction handleChargeAppliedTransaction(final LoanCharge loanCharge, final LocalDate suppliedTransactionDate) { - if (isProgressiveSchedule()) { - return null; - } - - return createChargeAppliedTransaction(loanCharge, suppliedTransactionDate); - } - - public LoanTransaction createChargeAppliedTransaction(final LoanCharge loanCharge, final LocalDate suppliedTransactionDate) { - final Money chargeAmount = loanCharge.getAmount(getCurrency()); - Money feeCharges = chargeAmount; - Money penaltyCharges = Money.zero(getCurrency()); - if (loanCharge.isPenaltyCharge()) { - penaltyCharges = chargeAmount; - feeCharges = Money.zero(getCurrency()); - } - - LocalDate transactionDate; - if (suppliedTransactionDate != null) { - transactionDate = suppliedTransactionDate; - } else { - transactionDate = loanCharge.getDueLocalDate(); - final LocalDate currentDate = DateUtils.getBusinessLocalDate(); - - // if loan charge is to be applied on a future date, the loan transaction would show today's date as applied - // date - if (transactionDate == null || DateUtils.isAfter(transactionDate, currentDate)) { - transactionDate = currentDate; - } - } - ExternalId externalId = ExternalId.empty(); - if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { - externalId = ExternalId.generate(); - } - final LoanTransaction applyLoanChargeTransaction = LoanTransaction.accrueLoanCharge(this, getOffice(), chargeAmount, - transactionDate, feeCharges, penaltyCharges, externalId); - - Integer installmentNumber = null; - final LoanRepaymentScheduleInstallment installmentForCharge = this.getRelatedRepaymentScheduleInstallment(loanCharge.getDueDate()); - if (installmentForCharge != null) { - installmentForCharge.updateAccrualPortion(installmentForCharge.getInterestAccrued(this.getCurrency()), - installmentForCharge.getFeeAccrued(this.getCurrency()).add(feeCharges), - installmentForCharge.getPenaltyAccrued(this.getCurrency()).add(penaltyCharges)); - installmentNumber = installmentForCharge.getInstallmentNumber(); - } - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(applyLoanChargeTransaction, loanCharge, - loanCharge.getAmount(getCurrency()).getAmount(), installmentNumber); - applyLoanChargeTransaction.getLoanChargesPaid().add(loanChargePaidBy); - addLoanTransaction(applyLoanChargeTransaction); - return applyLoanChargeTransaction; - } - public LocalDate getLastRepaymentPeriodDueDate(final boolean includeRecalculatedInterestComponent) { LocalDate lastRepaymentDate = getDisbursementDate(); List installments = getRepaymentScheduleInstallments(); @@ -699,34 +635,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return lastRepaymentDate; } - public void removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(final LoanCharge loanCharge) { - if (loanCharge.isDueAtDisbursement()) { - LoanTransaction transactionToRemove = null; - List transactions = getLoanTransactions(); - for (final LoanTransaction transaction : transactions) { - if (transaction.isRepaymentAtDisbursement() - && doesLoanChargePaidByContainLoanCharge(transaction.getLoanChargesPaid(), loanCharge)) { - final MonetaryCurrency currency = getCurrency(); - final Money chargeAmount = Money.of(currency, loanCharge.amount()); - if (transaction.isGreaterThan(chargeAmount)) { - final Money principalPortion = Money.zero(currency); - final Money interestPortion = Money.zero(currency); - final Money penaltyChargesPortion = Money.zero(currency); - - transaction.updateComponentsAndTotal(principalPortion, interestPortion, chargeAmount, penaltyChargesPortion); - - } else { - transactionToRemove = transaction; - } - } - } - - if (transactionToRemove != null) { - this.loanTransactions.remove(transactionToRemove); - } - } - } - public void removeDisbursementDetails(final long id) { this.disbursementDetails.remove(fetchLoanDisbursementsById(id)); } @@ -739,83 +647,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return disbursementDetails; } - private boolean doesLoanChargePaidByContainLoanCharge(Set loanChargePaidBys, LoanCharge loanCharge) { - return loanChargePaidBys.stream() // - .anyMatch(loanChargePaidBy -> loanChargePaidBy.getLoanCharge().equals(loanCharge)); - } - - public BigDecimal calculateAmountPercentageAppliedTo(final LoanCharge loanCharge) { - if (loanCharge.isOverdueInstallmentCharge()) { - return loanCharge.getAmountPercentageAppliedTo(); - } - - return switch (loanCharge.getChargeCalculation()) { - case PERCENT_OF_AMOUNT -> getDerivedAmountForCharge(loanCharge); - case PERCENT_OF_AMOUNT_AND_INTEREST -> { - final BigDecimal totalInterestCharged = getTotalInterest(); - if (isMultiDisburmentLoan() && loanCharge.isDisbursementCharge()) { - yield getTotalAllTrancheDisbursementAmount().getAmount().add(totalInterestCharged); - } else { - yield getPrincipal().getAmount().add(totalInterestCharged); - } - } - case PERCENT_OF_INTEREST -> getTotalInterest(); - case PERCENT_OF_DISBURSEMENT_AMOUNT -> { - if (loanCharge.getTrancheDisbursementCharge() != null) { - yield loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().principal(); - } else { - yield getPrincipal().getAmount(); - } - } - case INVALID, FLAT -> BigDecimal.ZERO; - }; - } - - private Money getTotalAllTrancheDisbursementAmount() { - Money amount = Money.zero(getCurrency()); - if (isMultiDisburmentLoan()) { - for (final LoanDisbursementDetails loanDisbursementDetail : getDisbursementDetails()) { - amount = amount.plus(loanDisbursementDetail.principal()); - } - } - return amount; + public boolean removeLoanTransaction(LoanTransaction transactionToRemove) { + return this.loanTransactions.remove(transactionToRemove); } public BigDecimal getTotalInterest() { return this.summary.calculateTotalInterestCharged(getRepaymentScheduleInstallments(), getCurrency()).getAmount(); } - public BigDecimal calculatePerInstallmentChargeAmount(final LoanCharge loanCharge) { - return calculatePerInstallmentChargeAmount(loanCharge.getChargeCalculation(), loanCharge.getPercentage()); - } - - public BigDecimal calculatePerInstallmentChargeAmount(final ChargeCalculationType calculationType, final BigDecimal percentage) { - Money amount = Money.zero(getCurrency()); - List installments = getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment installment : installments) { - amount = amount.plus(calculateInstallmentChargeAmount(calculationType, percentage, installment)); - } - return amount.getAmount(); - } - public BigDecimal getTotalWrittenOff() { return this.summary.getTotalWrittenOff(); } - private Money calculateInstallmentChargeAmount(final ChargeCalculationType calculationType, final BigDecimal percentage, - final LoanRepaymentScheduleInstallment installment) { - Money percentOf = switch (calculationType) { - case PERCENT_OF_AMOUNT -> installment.getPrincipal(getCurrency()); - case PERCENT_OF_AMOUNT_AND_INTEREST -> - installment.getPrincipal(getCurrency()).plus(installment.getInterestCharged(getCurrency())); - case PERCENT_OF_INTEREST -> installment.getInterestCharged(getCurrency()); - case PERCENT_OF_DISBURSEMENT_AMOUNT, INVALID, FLAT -> Money.zero(getCurrency()); - - }; - return Money.zero(getCurrency()) // - .plus(LoanCharge.percentageOf(percentOf.getAmount(), percentage)); - } - public Client client() { return this.client; } @@ -850,58 +693,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.transactionProcessingStrategyName = transactionProcessingStrategyName; } - public void updateLoanCharges(final Set loanCharges) { - List existingCharges = fetchAllLoanChargeIds(); - - /* Process new and updated charges **/ - for (final LoanCharge loanCharge : loanCharges) { - LoanCharge charge = loanCharge; - // add new charges - if (loanCharge.getId() == null) { - LoanTrancheDisbursementCharge loanTrancheDisbursementCharge; - loanCharge.update(this); - if (this.loanProduct.isMultiDisburseLoan() && loanCharge.isTrancheDisbursementCharge()) { - loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().updateLoan(this); - for (final LoanDisbursementDetails loanDisbursementDetails : getDisbursementDetails()) { - if (loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().getId() == null - && loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().equals(loanDisbursementDetails)) { - loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, loanDisbursementDetails); - loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge); - } - } - } - this.charges.add(loanCharge); - - } else { - charge = fetchLoanChargesById(charge.getId()); - if (charge != null) { - existingCharges.remove(charge.getId()); - } - } - final BigDecimal amount = calculateAmountPercentageAppliedTo(loanCharge); - BigDecimal chargeAmt; - BigDecimal totalChargeAmt = BigDecimal.ZERO; - if (loanCharge.getChargeCalculation().isPercentageBased()) { - chargeAmt = loanCharge.getPercentage(); - if (loanCharge.isInstalmentFee()) { - totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge); - } - } else { - chargeAmt = loanCharge.amountOrPercentage(); - } - if (charge != null) { - charge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt); - } - - } - - /* Updated deleted charges **/ - for (Long id : existingCharges) { - fetchLoanChargesById(id).setActive(false); - } - updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); - } - public void updateLoanCollateral(final Set loanCollateral) { if (this.loanCollateralManagements == null) { this.loanCollateralManagements = new HashSet<>(); @@ -955,49 +746,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.loanProduct.isMultiDisburseLoan(); } - /** - * Update interest recalculation settings if product configuration changes - */ - public void updateOverdueScheduleInstallment(final LoanCharge loanCharge) { - if (loanCharge.isOverdueInstallmentCharge() && loanCharge.isActive()) { - LoanOverdueInstallmentCharge overdueInstallmentCharge = loanCharge.getOverdueInstallmentCharge(); - if (overdueInstallmentCharge != null) { - Integer installmentNumber = overdueInstallmentCharge.getInstallment().getInstallmentNumber(); - LoanRepaymentScheduleInstallment installment = fetchRepaymentScheduleInstallment(installmentNumber); - overdueInstallmentCharge.updateLoanRepaymentScheduleInstallment(installment); - } - } - } - - public void clearLoanInstallmentChargesBeforeRegeneration(final LoanCharge loanCharge) { - /* - * JW https://issues.apache.org/jira/browse/FINERACT-1557 For loan installment charges only : Clear down - * installment charges from the loanCharge and from each of the repayment installments and allow them to be - * recalculated fully anew. This patch is to avoid the 'merging' of existing and regenerated installment charges - * which results in the installment charges being deleted on loan approval if the schedule is regenerated. Not - * pretty. updateInstallmentCharges in LoanCharge.java: the merging looks like it will work but doesn't so this - * patch simply hits the part which 'adds all' rather than merge. Possibly an ORM issue. The issue could be to - * do with the fact that, on approval, the "recalculateLoanCharge" happens twice (probably 2 schedule - * regenerations) whereas it only happens once on Submit and Disburse (and no problems with them) - * - * if (this.loanInstallmentCharge.isEmpty()) { this.loanInstallmentCharge.addAll(newChargeInstallments); - */ - Loan loan = loanCharge.getLoan(); - if (!loan.isSubmittedAndPendingApproval() && !loan.isApproved()) { - return; - } // doing for both just in case status is not - // updated at this points - if (loanCharge.isInstalmentFee()) { - loanCharge.clearLoanInstallmentCharges(); - for (final LoanRepaymentScheduleInstallment installment : getRepaymentScheduleInstallments()) { - if (installment.isRecalculatedInterestComponent()) { - continue; // JW: does this in generateInstallmentLoanCharges - but don't understand it - } - installment.getInstallmentCharges().clear(); - } - } - } - public BigDecimal calculateOverdueAmountPercentageAppliedTo(final LoanCharge loanCharge, final int penaltyWaitPeriod) { LoanRepaymentScheduleInstallment installment = loanCharge.getOverdueInstallmentCharge().getInstallment(); LocalDate graceDate = DateUtils.getBusinessLocalDate().minusDays(penaltyWaitPeriod); @@ -1166,26 +914,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { && this.loanRepaymentScheduleDetail.isEnableAutoRepaymentForDownPayment(); } - public void handlePayDisbursementTransaction(final Long chargeId, final LoanTransaction chargesPayment, - final List existingTransactionIds, final List existingReversedTransactionIds) { - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - LoanCharge charge = null; - for (final LoanCharge loanCharge : this.charges) { - if (loanCharge.isActive() && chargeId.equals(loanCharge.getId())) { - charge = loanCharge; - } - } - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount(), null); - chargesPayment.getLoanChargesPaid().add(loanChargePaidBy); - final Money zero = Money.zero(getCurrency()); - chargesPayment.updateComponents(zero, zero, charge.getAmount(getCurrency()), zero); - chargesPayment.updateLoan(this); - addLoanTransaction(chargesPayment); - updateLoanOutstandingBalances(); - charge.markAsFullyPaid(); - } - public void removePostDatedChecks() { this.postDatedChecks = new ArrayList<>(); } @@ -1306,31 +1034,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return possibleNextRepaymentDate; } - public LoanTransaction deriveDefaultInterestWaiverTransaction() { - final Money totalInterestOutstanding = getTotalInterestOutstandingOnLoan(); - Money possibleInterestToWaive = totalInterestOutstanding.copy(); - LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - - if (totalInterestOutstanding.isGreaterThanZero()) { - // find earliest known instance of overdue interest and default to - // that - List installments = getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) { - - final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(getCurrency()); - if (scheduledRepayment.isOverdueOn(DateUtils.getBusinessLocalDate()) && scheduledRepayment.isNotFullyPaidOff() - && outstandingForPeriod.isGreaterThanZero()) { - transactionDate = scheduledRepayment.getDueDate(); - possibleInterestToWaive = outstandingForPeriod; - break; - } - } - } - - return LoanTransaction.waiver(getOffice(), this, possibleInterestToWaive, transactionDate, possibleInterestToWaive, - possibleInterestToWaive.zero(), ExternalId.empty()); - } - public LoanTransaction findWriteOffTransaction() { return this.loanTransactions.stream() // .filter(transaction -> !transaction.isReversed() && transaction.isWriteOff()) // @@ -1717,119 +1420,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.syncDisbursementWithMeeting != null && this.syncDisbursementWithMeeting; } - public void updateLoanRepaymentScheduleDates(final String recurringRule, final boolean isHolidayEnabled, final List holidays, - final WorkingDays workingDays, final LocalDate presentMeetingDate, final LocalDate newMeetingDate, - final boolean isSkipRepaymentOnFirstDayOfMonth, final Integer numberOfDays) { - // first repayment's from date is same as disbursement date. - // meetingStartDate is used as seedDate Capture the seedDate from user and use the seedDate as meetingStart date - - LocalDate tmpFromDate = getDisbursementDate(); - final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType(); - final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery(); - final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); - - LocalDate newRepaymentDate; - boolean isFirstTime = true; - LocalDate latestRepaymentDate = null; - List installments = getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) { - LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); - if (!DateUtils.isBefore(oldDueDate, presentMeetingDate)) { - if (isFirstTime) { - isFirstTime = false; - newRepaymentDate = newMeetingDate; - } else { - // tmpFromDate.plusDays(1) is done to make sure - // getNewRepaymentMeetingDate method returns next meeting - // date and not the same as tmpFromDate - newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recurringRule, tmpFromDate, tmpFromDate.plusDays(1), - loanRepaymentInterval, frequency, workingDays, isSkipRepaymentOnFirstDayOfMonth, numberOfDays); - } - - if (isHolidayEnabled) { - newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays); - } - if (DateUtils.isBefore(latestRepaymentDate, newRepaymentDate)) { - latestRepaymentDate = newRepaymentDate; - } - loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); - // reset from date to get actual daysInPeriod - - loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); - - tmpFromDate = newRepaymentDate;// update with new repayment date - } else { - tmpFromDate = oldDueDate; - } - } - if (latestRepaymentDate != null) { - this.expectedMaturityDate = latestRepaymentDate; - } - } - - public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule, - final boolean isHolidayEnabled, final List holidays, final WorkingDays workingDays, - final boolean isSkipRepaymentonfirstdayofmonth, final Integer numberofDays) { - // first repayment's from date is same as disbursement date. - LocalDate tmpFromDate = getDisbursementDate(); - final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType(); - final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery(); - final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); - - LocalDate newRepaymentDate; - LocalDate latestRepaymentDate = null; - List installments = getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) { - LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); - - // FIXME: AA this won't update repayment dates before current date. - if (DateUtils.isAfter(oldDueDate, meetingStartDate) && DateUtils.isDateInTheFuture(oldDueDate)) { - newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, meetingStartDate, oldDueDate, - loanRepaymentInterval, frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays); - - final LocalDate maxDateLimitForNewRepayment = getMaxDateLimitForNewRepayment(repaymentPeriodFrequencyType, - loanRepaymentInterval, tmpFromDate); - - if (DateUtils.isAfter(newRepaymentDate, maxDateLimitForNewRepayment)) { - newRepaymentDate = CalendarUtils.getNextRepaymentMeetingDate(recuringRule, meetingStartDate, tmpFromDate, - loanRepaymentInterval, frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays); - } - - if (isHolidayEnabled) { - newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays); - } - if (DateUtils.isBefore(latestRepaymentDate, newRepaymentDate)) { - latestRepaymentDate = newRepaymentDate; - } - - loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); - // reset from date to get actual daysInPeriod - loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); - tmpFromDate = newRepaymentDate;// update with new repayment date - } else { - tmpFromDate = oldDueDate; - } - } - if (latestRepaymentDate != null) { - this.expectedMaturityDate = latestRepaymentDate; - } - } - - private LocalDate getMaxDateLimitForNewRepayment(final PeriodFrequencyType periodFrequencyType, final Integer loanRepaymentInterval, - final LocalDate startDate) { - LocalDate dueRepaymentPeriodDate = startDate; - final int repaidEvery = 2 * loanRepaymentInterval; - switch (periodFrequencyType) { - case DAYS -> dueRepaymentPeriodDate = startDate.plusDays(repaidEvery); - case WEEKS -> dueRepaymentPeriodDate = startDate.plusWeeks(repaidEvery); - case MONTHS -> dueRepaymentPeriodDate = startDate.plusMonths(repaidEvery); - case YEARS -> dueRepaymentPeriodDate = startDate.plusYears(repaidEvery); - case INVALID, WHOLE_TERM -> { - } - } - return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from startDate - } - public Group group() { return this.group; } @@ -1917,48 +1507,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.charges == null ? new HashSet<>() : this.charges.stream().filter(LoanCharge::isActive).collect(Collectors.toSet()); } - public List generateInstallmentLoanCharges(final LoanCharge loanCharge) { - final List loanChargePerInstallments = new ArrayList<>(); - if (loanCharge.isInstalmentFee()) { - List installments = getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment installment : installments) { - if (installment.isRecalculatedInterestComponent()) { - continue; - } - BigDecimal amount; - if (loanCharge.getChargeCalculation().isFlat()) { - amount = loanCharge.amountOrPercentage(); - } else { - amount = calculateInstallmentChargeAmount(loanCharge.getChargeCalculation(), loanCharge.getPercentage(), installment) - .getAmount(); - } - final LoanInstallmentCharge loanInstallmentCharge = new LoanInstallmentCharge(amount, loanCharge, installment); - installment.getInstallmentCharges().add(loanInstallmentCharge); - loanChargePerInstallments.add(loanInstallmentCharge); - } - } - return loanChargePerInstallments; - } - - public LoanCharge fetchLoanChargesById(Long id) { - LoanCharge charge = null; - for (LoanCharge loanCharge : this.charges) { - if (id.equals(loanCharge.getId())) { - charge = loanCharge; - break; - } - } - return charge; - } - - private List fetchAllLoanChargeIds() { - List list = new ArrayList<>(); - for (LoanCharge loanCharge : this.charges) { - list.add(loanCharge.getId()); - } - return list; - } - public List getAllDisbursementDetails() { return this.disbursementDetails; } @@ -2315,26 +1863,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return nextRepaymentDate; } - public BigDecimal getDerivedAmountForCharge(final LoanCharge loanCharge) { - BigDecimal amount = BigDecimal.ZERO; - if (isMultiDisburmentLoan() && loanCharge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue())) { - amount = getApprovedPrincipal(); - } else { - // If charge type is specified due date and loan is multi disburment loan. - // Then we need to get as of this loan charge due date how much amount disbursed. - if (loanCharge.isSpecifiedDueDate() && this.isMultiDisburmentLoan()) { - for (final LoanDisbursementDetails loanDisbursementDetails : this.getDisbursementDetails()) { - if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), loanCharge.getDueDate())) { - amount = amount.add(loanDisbursementDetails.principal()); - } - } - } else { - amount = getPrincipal().getAmount(); - } - } - return amount; - } - public void updateWriteOffReason(CodeValue writeOffReason) { this.writeOffReason = writeOffReason; } @@ -2580,6 +2108,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return Optional.ofNullable(this.charges).orElse(new HashSet<>()); } + public void addCharge(LoanCharge loanCharge) { + this.getCharges().add(loanCharge); + } + public void removeCharges(Predicate predicate) { charges.removeIf(predicate); } @@ -2687,4 +2219,5 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public boolean hasAccelerateChargeOffStrategy() { return LoanChargeOffBehaviour.ACCELERATE_MATURITY.equals(getLoanProductRelatedDetail().getChargeOffBehaviour()); } + } 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 880932828..8715b859b 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 @@ -434,7 +434,7 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { this.amountPercentageAppliedTo = amount; loanCharge = BigDecimal.ZERO; if (isInstalmentFee()) { - loanCharge = this.loan.calculatePerInstallmentChargeAmount(ChargeCalculationType.fromInt(this.chargeCalculation), + loanCharge = calculatePerInstallmentChargeAmount(loan, ChargeCalculationType.fromInt(this.chargeCalculation), this.percentage); } if (loanCharge.compareTo(BigDecimal.ZERO) == 0) { @@ -452,9 +452,67 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom { return actualChanges; } + public static BigDecimal calculatePerInstallmentChargeAmount(final Loan loan, final ChargeCalculationType calculationType, + final BigDecimal percentage) { + Money amount = Money.zero(loan.getCurrency()); + List installments = loan.getRepaymentScheduleInstallments(); + for (final LoanRepaymentScheduleInstallment installment : installments) { + amount = amount.plus(calculateInstallmentChargeAmount(loan, calculationType, percentage, installment)); + } + return amount.getAmount(); + } + + public BigDecimal calculatePerInstallmentChargeAmount() { + Loan loan = this.loan; + Money amount = Money.zero(loan.getCurrency()); + List installments = loan.getRepaymentScheduleInstallments(); + for (final LoanRepaymentScheduleInstallment installment : installments) { + amount = amount.plus(calculateInstallmentChargeAmount(loan, getChargeCalculation(), getPercentage(), installment)); + } + return amount.getAmount(); + } + + private List generateInstallmentLoanCharges(final LoanCharge loanCharge) { + Loan loan = this.loan; + final List loanChargePerInstallments = new ArrayList<>(); + if (loanCharge.isInstalmentFee()) { + List installments = loan.getRepaymentScheduleInstallments(); + for (final LoanRepaymentScheduleInstallment installment : installments) { + if (installment.isRecalculatedInterestComponent()) { + continue; + } + BigDecimal amount; + if (loanCharge.getChargeCalculation().isFlat()) { + amount = loanCharge.amountOrPercentage(); + } else { + amount = calculateInstallmentChargeAmount(loan, loanCharge.getChargeCalculation(), loanCharge.getPercentage(), + installment).getAmount(); + } + final LoanInstallmentCharge loanInstallmentCharge = new LoanInstallmentCharge(amount, loanCharge, installment); + installment.getInstallmentCharges().add(loanInstallmentCharge); + loanChargePerInstallments.add(loanInstallmentCharge); + } + } + return loanChargePerInstallments; + } + + private static Money calculateInstallmentChargeAmount(final Loan loan, final ChargeCalculationType calculationType, + final BigDecimal percentage, final LoanRepaymentScheduleInstallment installment) { + Money percentOf = switch (calculationType) { + case PERCENT_OF_AMOUNT -> installment.getPrincipal(loan.getCurrency()); + case PERCENT_OF_AMOUNT_AND_INTEREST -> + installment.getPrincipal(loan.getCurrency()).plus(installment.getInterestCharged(loan.getCurrency())); + case PERCENT_OF_INTEREST -> installment.getInterestCharged(loan.getCurrency()); + case PERCENT_OF_DISBURSEMENT_AMOUNT, INVALID, FLAT -> Money.zero(loan.getCurrency()); + + }; + return Money.zero(loan.getCurrency()) // + .plus(LoanCharge.percentageOf(percentOf.getAmount(), percentage)); + } + private void updateInstallmentCharges() { final Collection remove = new HashSet<>(); - final List newChargeInstallments = this.loan.generateInstallmentLoanCharges(this); + final List newChargeInstallments = generateInstallmentLoanCharges(this); if (this.loanInstallmentCharge.isEmpty()) { this.loanInstallmentCharge.addAll(newChargeInstallments); } else { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java index 1383987e7..e28faaf1b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java @@ -19,18 +19,27 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; 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.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; +import org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; @@ -61,23 +70,52 @@ public class LoanChargeService { if (loanCharge.isOverdueInstallmentCharge()) { amount = loan.calculateOverdueAmountPercentageAppliedTo(loanCharge, penaltyWaitPeriod); } else { - amount = loan.calculateAmountPercentageAppliedTo(loanCharge); + amount = calculateAmountPercentageAppliedTo(loan, loanCharge); } chargeAmt = loanCharge.getPercentage(); if (loanCharge.isInstalmentFee()) { - totalChargeAmt = loan.calculatePerInstallmentChargeAmount(loanCharge); + totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge); } } else { chargeAmt = loanCharge.amountOrPercentage(); } if (loanCharge.isActive()) { - loan.clearLoanInstallmentChargesBeforeRegeneration(loanCharge); + clearLoanInstallmentChargesBeforeRegeneration(loanCharge); loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt); loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate()); } } + private void clearLoanInstallmentChargesBeforeRegeneration(final LoanCharge loanCharge) { + /* + * JW https://issues.apache.org/jira/browse/FINERACT-1557 For loan installment charges only : Clear down + * installment charges from the loanCharge and from each of the repayment installments and allow them to be + * recalculated fully anew. This patch is to avoid the 'merging' of existing and regenerated installment charges + * which results in the installment charges being deleted on loan approval if the schedule is regenerated. Not + * pretty. updateInstallmentCharges in LoanCharge.java: the merging looks like it will work but doesn't so this + * patch simply hits the part which 'adds all' rather than merge. Possibly an ORM issue. The issue could be to + * do with the fact that, on approval, the "recalculateLoanCharge" happens twice (probably 2 schedule + * regenerations) whereas it only happens once on Submit and Disburse (and no problems with them) + * + * if (this.loanInstallmentCharge.isEmpty()) { this.loanInstallmentCharge.addAll(newChargeInstallments); + */ + Loan loan = loanCharge.getLoan(); + if (!loan.isSubmittedAndPendingApproval() && !loan.isApproved()) { + return; + } // doing for both just in case status is not + // updated at this points + if (loanCharge.isInstalmentFee()) { + loanCharge.clearLoanInstallmentCharges(); + for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { + if (installment.isRecalculatedInterestComponent()) { + continue; // JW: does this in generateInstallmentLoanCharges - but don't understand it + } + installment.getInstallmentCharges().clear(); + } + } + } + public void makeChargePayment(final Loan loan, final Long chargeId, final List existingTransactionIds, final List existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) { loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction); @@ -122,16 +160,77 @@ public class LoanChargeService { loanLifecycleStateMachine.determineAndTransition(loan, chargesPayment.getTransactionDate()); } + /** + * Creates a loanTransaction for "Apply Charge Event" with transaction date set to "suppliedTransactionDate". The + * newly created transaction is also added to the Loan on which this method is called. + * + * If "suppliedTransactionDate" is not passed Id, the transaction date is set to the loans due date if the due date + * is lesser than today's date. If not, the transaction date is set to today's date + */ + public LoanTransaction handleChargeAppliedTransaction(final Loan loan, final LoanCharge loanCharge, + final LocalDate suppliedTransactionDate) { + if (loan.isProgressiveSchedule()) { + return null; + } + + return createChargeAppliedTransaction(loan, loanCharge, suppliedTransactionDate); + } + + public LoanTransaction createChargeAppliedTransaction(final Loan loan, final LoanCharge loanCharge, + final LocalDate suppliedTransactionDate) { + final Money chargeAmount = loanCharge.getAmount(loan.getCurrency()); + Money feeCharges = chargeAmount; + Money penaltyCharges = Money.zero(loan.getCurrency()); + if (loanCharge.isPenaltyCharge()) { + penaltyCharges = chargeAmount; + feeCharges = Money.zero(loan.getCurrency()); + } + + LocalDate transactionDate; + if (suppliedTransactionDate != null) { + transactionDate = suppliedTransactionDate; + } else { + transactionDate = loanCharge.getDueLocalDate(); + final LocalDate currentDate = DateUtils.getBusinessLocalDate(); + + // if loan charge is to be applied on a future date, the loan transaction would show today's date as applied + // date + if (transactionDate == null || DateUtils.isAfter(transactionDate, currentDate)) { + transactionDate = currentDate; + } + } + ExternalId externalId = ExternalId.empty(); + if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + final LoanTransaction applyLoanChargeTransaction = LoanTransaction.accrueLoanCharge(loan, loan.getOffice(), chargeAmount, + transactionDate, feeCharges, penaltyCharges, externalId); + + Integer installmentNumber = null; + final LoanRepaymentScheduleInstallment installmentForCharge = loan.getRelatedRepaymentScheduleInstallment(loanCharge.getDueDate()); + if (installmentForCharge != null) { + installmentForCharge.updateAccrualPortion(installmentForCharge.getInterestAccrued(loan.getCurrency()), + installmentForCharge.getFeeAccrued(loan.getCurrency()).add(feeCharges), + installmentForCharge.getPenaltyAccrued(loan.getCurrency()).add(penaltyCharges)); + installmentNumber = installmentForCharge.getInstallmentNumber(); + } + final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(applyLoanChargeTransaction, loanCharge, + loanCharge.getAmount(loan.getCurrency()).getAmount(), installmentNumber); + applyLoanChargeTransaction.getLoanChargesPaid().add(loanChargePaidBy); + loan.addLoanTransaction(applyLoanChargeTransaction); + return applyLoanChargeTransaction; + } + public void addLoanCharge(final Loan loan, final LoanCharge loanCharge) { loanCharge.update(loan); - final BigDecimal amount = loan.calculateAmountPercentageAppliedTo(loanCharge); + final BigDecimal amount = calculateAmountPercentageAppliedTo(loan, loanCharge); BigDecimal chargeAmt; BigDecimal totalChargeAmt = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageBased()) { chargeAmt = loanCharge.getPercentage(); if (loanCharge.isInstalmentFee()) { - totalChargeAmt = loan.calculatePerInstallmentChargeAmount(loanCharge); + totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge); } else if (loanCharge.isOverdueInstallmentCharge()) { totalChargeAmt = loanCharge.amountOutstanding(); } @@ -156,4 +255,151 @@ public class LoanChargeService { loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, loan); } + public void updateLoanCharges(final Loan loan, final Set loanCharges) { + List existingCharges = fetchAllLoanChargeIds(loan); + + /* Process new and updated charges **/ + for (final LoanCharge loanCharge : loanCharges) { + LoanCharge charge = loanCharge; + // add new charges + if (loanCharge.getId() == null) { + LoanTrancheDisbursementCharge loanTrancheDisbursementCharge; + loanCharge.update(loan); + if (loan.getLoanProduct().isMultiDisburseLoan() && loanCharge.isTrancheDisbursementCharge()) { + loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().updateLoan(loan); + for (final LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) { + if (loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().getId() == null + && loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().equals(loanDisbursementDetails)) { + loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, loanDisbursementDetails); + loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge); + } + } + } + loan.addCharge(loanCharge); + + } else { + charge = fetchLoanChargesById(loan, charge.getId()); + if (charge != null) { + existingCharges.remove(charge.getId()); + } + } + final BigDecimal amount = calculateAmountPercentageAppliedTo(loan, loanCharge); + BigDecimal chargeAmt; + BigDecimal totalChargeAmt = BigDecimal.ZERO; + if (loanCharge.getChargeCalculation().isPercentageBased()) { + chargeAmt = loanCharge.getPercentage(); + if (loanCharge.isInstalmentFee()) { + totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge); + } + } else { + chargeAmt = loanCharge.amountOrPercentage(); + } + if (charge != null) { + charge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), + totalChargeAmt); + } + + } + + /* Updated deleted charges **/ + for (Long id : existingCharges) { + fetchLoanChargesById(loan, id).setActive(false); + } + loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()); + } + + private List fetchAllLoanChargeIds(final Loan loan) { + List list = new ArrayList<>(); + for (LoanCharge loanCharge : loan.getCharges()) { + list.add(loanCharge.getId()); + } + return list; + } + + public BigDecimal calculateAmountPercentageAppliedTo(final Loan loan, final LoanCharge loanCharge) { + if (loanCharge.isOverdueInstallmentCharge()) { + return loanCharge.getAmountPercentageAppliedTo(); + } + + return switch (loanCharge.getChargeCalculation()) { + case PERCENT_OF_AMOUNT -> getDerivedAmountForCharge(loan, loanCharge); + case PERCENT_OF_AMOUNT_AND_INTEREST -> { + final BigDecimal totalInterestCharged = loan.getTotalInterest(); + if (loan.isMultiDisburmentLoan() && loanCharge.isDisbursementCharge()) { + yield getTotalAllTrancheDisbursementAmount(loan).getAmount().add(totalInterestCharged); + } else { + yield loan.getPrincipal().getAmount().add(totalInterestCharged); + } + } + case PERCENT_OF_INTEREST -> loan.getTotalInterest(); + case PERCENT_OF_DISBURSEMENT_AMOUNT -> { + if (loanCharge.getTrancheDisbursementCharge() != null) { + yield loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().principal(); + } else { + yield loan.getPrincipal().getAmount(); + } + } + case INVALID, FLAT -> BigDecimal.ZERO; + }; + } + + private BigDecimal getDerivedAmountForCharge(final Loan loan, final LoanCharge loanCharge) { + BigDecimal amount = BigDecimal.ZERO; + if (loan.isMultiDisburmentLoan() && loanCharge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue())) { + amount = loan.getApprovedPrincipal(); + } else { + // If charge type is specified due date and loan is multi disburment loan. + // Then we need to get as of this loan charge due date how much amount disbursed. + if (loanCharge.isSpecifiedDueDate() && loan.isMultiDisburmentLoan()) { + for (final LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) { + if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDate(), loanCharge.getDueDate())) { + amount = amount.add(loanDisbursementDetails.principal()); + } + } + } else { + amount = loan.getPrincipal().getAmount(); + } + } + return amount; + } + + private Money getTotalAllTrancheDisbursementAmount(final Loan loan) { + Money amount = Money.zero(loan.getCurrency()); + if (loan.isMultiDisburmentLoan()) { + for (final LoanDisbursementDetails loanDisbursementDetail : loan.getDisbursementDetails()) { + amount = amount.plus(loanDisbursementDetail.principal()); + } + } + return amount; + } + + private BigDecimal calculatePerInstallmentChargeAmount(final LoanCharge loanCharge) { + return loanCharge.calculatePerInstallmentChargeAmount(); + } + + public LoanCharge fetchLoanChargesById(final Loan loan, final Long id) { + LoanCharge charge = null; + for (LoanCharge loanCharge : loan.getCharges()) { + if (id.equals(loanCharge.getId())) { + charge = loanCharge; + break; + } + } + return charge; + } + + /** + * Update interest recalculation settings if product configuration changes + */ + public void updateOverdueScheduleInstallment(final Loan loan, final LoanCharge loanCharge) { + if (loanCharge.isOverdueInstallmentCharge() && loanCharge.isActive()) { + LoanOverdueInstallmentCharge overdueInstallmentCharge = loanCharge.getOverdueInstallmentCharge(); + if (overdueInstallmentCharge != null) { + Integer installmentNumber = overdueInstallmentCharge.getInstallment().getInstallmentNumber(); + LoanRepaymentScheduleInstallment installment = loan.fetchRepaymentScheduleInstallment(installmentNumber); + overdueInstallmentCharge.updateLoanRepaymentScheduleInstallment(installment); + } + } + } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java index 965c6c0c7..90140a171 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java @@ -101,7 +101,7 @@ public class LoanScheduleService { final Set charges = loan.getActiveCharges(); for (final LoanCharge loanCharge : charges) { if (!loanCharge.isDueAtDisbursement()) { - loan.updateOverdueScheduleInstallment(loanCharge); + loanChargeService.updateOverdueScheduleInstallment(loan, loanCharge); if (loanCharge.getDueLocalDate() == null || (!DateUtils.isBefore(lastRepaymentDate, loanCharge.getDueLocalDate()) || loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE))) { if ((loanCharge.isInstalmentFee() || !loanCharge.isWaived()) && (loanCharge.getDueLocalDate() == null diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 0a748f244..4507d5c41 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -397,7 +397,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { transactionDate, txnExternalId, loanTransactionType); if (loanTransactionType.isRepaymentAtDisbursement()) { - loan.handlePayDisbursementTransaction(chargeId, newPaymentTransaction, existingTransactionIds, existingReversedTransactionIds); + handlePayDisbursementTransaction(loan, chargeId, newPaymentTransaction, existingTransactionIds, existingReversedTransactionIds); } else { final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), transactionDate, @@ -438,6 +438,26 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { return newPaymentTransaction; } + private void handlePayDisbursementTransaction(final Loan loan, final Long chargeId, final LoanTransaction chargesPayment, + final List existingTransactionIds, final List existingReversedTransactionIds) { + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + LoanCharge charge = null; + for (final LoanCharge loanCharge : loan.getCharges()) { + if (loanCharge.isActive() && chargeId.equals(loanCharge.getId())) { + charge = loanCharge; + } + } + final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount(), null); + chargesPayment.getLoanChargesPaid().add(loanChargePaidBy); + final Money zero = Money.zero(loan.getCurrency()); + chargesPayment.updateComponents(zero, zero, charge.getAmount(loan.getCurrency()), zero); + chargesPayment.updateLoan(loan); + loan.addLoanTransaction(chargesPayment); + loan.updateLoanOutstandingBalances(); + charge.markAsFullyPaid(); + } + private void postJournalEntries(final Loan loanAccount, final List existingTransactionIds, final List existingReversedTransactionIds, boolean isAccountTransfer) { postJournalEntries(loanAccount, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, false); 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 a6bcd8178..a42326221 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 @@ -112,6 +112,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing private final ThreadPoolTaskExecutor taskExecutor; private final TransactionTemplate transactionTemplate; private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; + private final LoanChargeService loanChargeService; /** * method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan @@ -733,7 +734,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing continue; } final BigDecimal chargeAmount = MathUtil.toBigDecimal(chargePortion); - final LoanCharge loanCharge = loan.fetchLoanChargesById(accrualCharge.getLoanChargeId()); + final LoanCharge loanCharge = loanChargeService.fetchLoanChargesById(loan, accrualCharge.getLoanChargeId()); final LoanChargePaidBy paidBy = new LoanChargePaidBy(transaction, loanCharge, chargeAmount, installment.getInstallmentNumber()); loanCharge.getLoanChargePaidBySet().add(paidBy); transaction.getLoanChargesPaid().add(paidBy); @@ -845,7 +846,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing Money chargeAmount = loanCharge.getAmount(loan.getCurrency()); if (chargeAmount.isNotEqualTo(accrualTransaction.getAmount(loan.getCurrency()))) { accrualTransaction.reverse(); - loan.handleChargeAppliedTransaction(loanCharge, accrualTransaction.getTransactionDate()); + loanChargeService.handleChargeAppliedTransaction(loan, loanCharge, accrualTransaction.getTransactionDate()); } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java index 6ef88ae97..a6e6ebc3d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java @@ -832,7 +832,7 @@ public class LoanAssemblerImpl implements LoanAssembler { final String chargesParamName = "charges"; if (changes.containsKey(chargesParamName)) { - loan.updateLoanCharges(possiblyModifiedLoanCharges); + loanChargeService.updateLoanCharges(loan, possiblyModifiedLoanCharges); } // update installment level delinquency diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java index 6f205005f..98ff8f82e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeAssembler.java @@ -296,8 +296,8 @@ public class LoanChargeAssembler { if (percentage == null) { percentage = chargeDefinition.getAmount(); } - loanCharge = loan.calculatePerInstallmentChargeAmount(ChargeCalculationType.fromInt(chargeDefinition.getChargeCalculation()), - percentage); + loanCharge = LoanCharge.calculatePerInstallmentChargeAmount(loan, + ChargeCalculationType.fromInt(chargeDefinition.getChargeCalculation()), percentage); } // If charge type is specified due date and loan is multi disburment diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index c47752d87..48ab671b5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -474,7 +474,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo final Map changes = new LinkedHashMap<>(3); if (loan.getActiveCharges().contains(loanCharge)) { - final BigDecimal amount = loan.calculateAmountPercentageAppliedTo(loanCharge); + final BigDecimal amount = loanChargeService.calculateAmountPercentageAppliedTo(loan, loanCharge); final Map loanChargeChanges = loanCharge.update(command, amount); changes.putAll(loanChargeChanges); loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()); @@ -1115,7 +1115,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo // the loan product uses Upfront Accruals, or only when the loan are closed too, if ((loan.getStatus().isActive() && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) || loan.getStatus().isOverpaid() || loan.getStatus().isClosedObligationsMet()) { - final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null); + final LoanTransaction applyLoanChargeTransaction = loanChargeService.handleChargeAppliedTransaction(loan, loanCharge, null); if (applyLoanChargeTransaction != null) { this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction); businessEventNotifierService @@ -1123,7 +1123,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo } } else if (configurationDomainService.isImmediateChargeAccrualPostMaturityEnabled() && DateUtils.getBusinessLocalDate().isAfter(loan.getMaturityDate())) { - final LoanTransaction loanTransaction = loan.createChargeAppliedTransaction(loanCharge, null); + final LoanTransaction loanTransaction = loanChargeService.createChargeAppliedTransaction(loan, loanCharge, null); this.loanTransactionRepository.saveAndFlush(loanTransaction); businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(loanTransaction)); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java index a58fad1f0..9edd3b22c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java @@ -204,7 +204,7 @@ public class LoanDisbursementService { } } else if (disbursedOn.equals(loan.getActualDisbursementDate()) && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { - loan.handleChargeAppliedTransaction(charge, disbursedOn); + loanChargeService.handleChargeAppliedTransaction(loan, charge, disbursedOn); } } @@ -273,7 +273,7 @@ public class LoanDisbursementService { } else { if (!loanChargeIds.isEmpty() && loanChargeIds.size() != chargeIdLength) { for (Long chargeId : loanChargeIds) { - final LoanCharge deleteCharge = loan.fetchLoanChargesById(chargeId); + final LoanCharge deleteCharge = loanChargeService.fetchLoanChargesById(loan, chargeId); if (loan.getCharges().contains(deleteCharge)) { loanChargeValidator.validateLoanIsNotClosed(loan, deleteCharge); loanChargeValidator.validateLoanChargeIsNotWaived(loan, deleteCharge); 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 1ec838ec7..0a6a793f1 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 @@ -530,7 +530,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); final CurrencyData currencyData = applicationCurrency.toData(); - final LoanTransaction waiveOfInterest = loan.deriveDefaultInterestWaiverTransaction(); + final LoanTransaction waiveOfInterest = deriveDefaultInterestWaiverTransaction(loan); final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.WAIVE_INTEREST); @@ -2235,4 +2235,29 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa public boolean existsByLoanId(Long loanId) { return loanRepositoryWrapper.existsByLoanId(loanId); } + + private LoanTransaction deriveDefaultInterestWaiverTransaction(Loan loan) { + final Money totalInterestOutstanding = loan.getTotalInterestOutstandingOnLoan(); + Money possibleInterestToWaive = totalInterestOutstanding.copy(); + LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + + if (totalInterestOutstanding.isGreaterThanZero()) { + // find earliest known instance of overdue interest and default to + // that + List installments = loan.getRepaymentScheduleInstallments(); + for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) { + + final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(loan.getCurrency()); + if (scheduledRepayment.isOverdueOn(DateUtils.getBusinessLocalDate()) && scheduledRepayment.isNotFullyPaidOff() + && outstandingForPeriod.isGreaterThanZero()) { + transactionDate = scheduledRepayment.getDueDate(); + possibleInterestToWaive = outstandingForPeriod; + break; + } + } + } + + return LoanTransaction.waiver(loan.getOffice(), loan, possibleInterestToWaive, transactionDate, possibleInterestToWaive, + possibleInterestToWaive.zero(), ExternalId.empty()); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 88ce0dadd..51ce03452 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -107,6 +107,7 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.holiday.domain.Holiday; import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; +import org.apache.fineract.organisation.holiday.service.HolidayUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.office.domain.Office; @@ -139,12 +140,14 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository; import org.apache.fineract.portfolio.calendar.domain.CalendarRepository; import org.apache.fineract.portfolio.calendar.domain.CalendarType; import org.apache.fineract.portfolio.calendar.exception.CalendarParameterUpdateNotSupportedException; +import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand; import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand; import org.apache.fineract.portfolio.collectionsheet.command.SingleDisbursalCommand; import org.apache.fineract.portfolio.collectionsheet.command.SingleRepaymentCommand; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; @@ -2003,10 +2006,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf existingReversedTransactionIds); createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); } else if (rescheduleBasedOnMeetingDates != null && rescheduleBasedOnMeetingDates) { - loan.updateLoanRepaymentScheduleDates(calendar.getRecurrence(), isHolidayEnabled, holidays, workingDays, + updateLoanRepaymentScheduleDates(loan, calendar.getRecurrence(), isHolidayEnabled, holidays, workingDays, presentMeetingDate, newMeetingDate, isSkipRepaymentOnFirstMonth, numberOfDays); } else { - loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled, + updateLoanRepaymentScheduleDates(loan, calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled, holidays, workingDays, isSkipRepaymentOnFirstMonth, numberOfDays); } @@ -2021,6 +2024,119 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } + public void updateLoanRepaymentScheduleDates(final Loan loan, final LocalDate meetingStartDate, final String recuringRule, + final boolean isHolidayEnabled, final List holidays, final WorkingDays workingDays, + final boolean isSkipRepaymentonfirstdayofmonth, final Integer numberofDays) { + // first repayment's from date is same as disbursement date. + LocalDate tmpFromDate = loan.getDisbursementDate(); + final PeriodFrequencyType repaymentPeriodFrequencyType = loan.getLoanRepaymentScheduleDetail().getRepaymentPeriodFrequencyType(); + final Integer loanRepaymentInterval = loan.getLoanRepaymentScheduleDetail().getRepayEvery(); + final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); + + LocalDate newRepaymentDate; + LocalDate latestRepaymentDate = null; + List installments = loan.getRepaymentScheduleInstallments(); + for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) { + LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); + + // FIXME: AA this won't update repayment dates before current date. + if (DateUtils.isAfter(oldDueDate, meetingStartDate) && DateUtils.isDateInTheFuture(oldDueDate)) { + newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, meetingStartDate, oldDueDate, + loanRepaymentInterval, frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays); + + final LocalDate maxDateLimitForNewRepayment = getMaxDateLimitForNewRepayment(repaymentPeriodFrequencyType, + loanRepaymentInterval, tmpFromDate); + + if (DateUtils.isAfter(newRepaymentDate, maxDateLimitForNewRepayment)) { + newRepaymentDate = CalendarUtils.getNextRepaymentMeetingDate(recuringRule, meetingStartDate, tmpFromDate, + loanRepaymentInterval, frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays); + } + + if (isHolidayEnabled) { + newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays); + } + if (DateUtils.isBefore(latestRepaymentDate, newRepaymentDate)) { + latestRepaymentDate = newRepaymentDate; + } + + loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); + // reset from date to get actual daysInPeriod + loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); + tmpFromDate = newRepaymentDate;// update with new repayment date + } else { + tmpFromDate = oldDueDate; + } + } + if (latestRepaymentDate != null) { + loan.setExpectedMaturityDate(latestRepaymentDate); + } + } + + private LocalDate getMaxDateLimitForNewRepayment(final PeriodFrequencyType periodFrequencyType, final Integer loanRepaymentInterval, + final LocalDate startDate) { + LocalDate dueRepaymentPeriodDate = startDate; + final int repaidEvery = 2 * loanRepaymentInterval; + switch (periodFrequencyType) { + case DAYS -> dueRepaymentPeriodDate = startDate.plusDays(repaidEvery); + case WEEKS -> dueRepaymentPeriodDate = startDate.plusWeeks(repaidEvery); + case MONTHS -> dueRepaymentPeriodDate = startDate.plusMonths(repaidEvery); + case YEARS -> dueRepaymentPeriodDate = startDate.plusYears(repaidEvery); + case INVALID, WHOLE_TERM -> { + } + } + return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from startDate + } + + private void updateLoanRepaymentScheduleDates(final Loan loan, final String recurringRule, final boolean isHolidayEnabled, + final List holidays, final WorkingDays workingDays, final LocalDate presentMeetingDate, final LocalDate newMeetingDate, + final boolean isSkipRepaymentOnFirstDayOfMonth, final Integer numberOfDays) { + // first repayment's from date is same as disbursement date. + // meetingStartDate is used as seedDate Capture the seedDate from user and use the seedDate as meetingStart date + + LocalDate tmpFromDate = loan.getDisbursementDate(); + final PeriodFrequencyType repaymentPeriodFrequencyType = loan.getLoanRepaymentScheduleDetail().getRepaymentPeriodFrequencyType(); + final Integer loanRepaymentInterval = loan.getLoanRepaymentScheduleDetail().getRepayEvery(); + final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType); + + LocalDate newRepaymentDate; + boolean isFirstTime = true; + LocalDate latestRepaymentDate = null; + List installments = loan.getRepaymentScheduleInstallments(); + for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) { + LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate(); + if (!DateUtils.isBefore(oldDueDate, presentMeetingDate)) { + if (isFirstTime) { + isFirstTime = false; + newRepaymentDate = newMeetingDate; + } else { + // tmpFromDate.plusDays(1) is done to make sure + // getNewRepaymentMeetingDate method returns next meeting + // date and not the same as tmpFromDate + newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recurringRule, tmpFromDate, tmpFromDate.plusDays(1), + loanRepaymentInterval, frequency, workingDays, isSkipRepaymentOnFirstDayOfMonth, numberOfDays); + } + + if (isHolidayEnabled) { + newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays); + } + if (DateUtils.isBefore(latestRepaymentDate, newRepaymentDate)) { + latestRepaymentDate = newRepaymentDate; + } + loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate); + // reset from date to get actual daysInPeriod + + loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); + + tmpFromDate = newRepaymentDate;// update with new repayment date + } else { + tmpFromDate = oldDueDate; + } + } + if (latestRepaymentDate != null) { + loan.setExpectedMaturityDate(latestRepaymentDate); + } + } + private void removeLoanCycle(final Loan loan) { final List loansToUpdate; if (loan.isGroupLoan()) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java index 1d4612ce9..6e9757e7e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ReprocessLoanTransactionsServiceImpl.java @@ -21,14 +21,18 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.time.LocalDate; import java.util.List; import java.util.Optional; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.interestpauses.service.LoanAccountTransfersService; import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountService; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; @@ -83,7 +87,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()); } - loan.removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(loanCharge); + removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(loan, loanCharge); if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loan.getCurrency())) { /* @@ -100,6 +104,39 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa loan.updateLoanSummaryDerivedFields(); } + private void removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(final Loan loan, final LoanCharge loanCharge) { + if (loanCharge.isDueAtDisbursement()) { + LoanTransaction transactionToRemove = null; + List transactions = loan.getLoanTransactions(); + for (final LoanTransaction transaction : transactions) { + if (transaction.isRepaymentAtDisbursement() + && doesLoanChargePaidByContainLoanCharge(transaction.getLoanChargesPaid(), loanCharge)) { + final MonetaryCurrency currency = loan.getCurrency(); + final Money chargeAmount = Money.of(currency, loanCharge.amount()); + if (transaction.isGreaterThan(chargeAmount)) { + final Money principalPortion = Money.zero(currency); + final Money interestPortion = Money.zero(currency); + final Money penaltyChargesPortion = Money.zero(currency); + + transaction.updateComponentsAndTotal(principalPortion, interestPortion, chargeAmount, penaltyChargesPortion); + + } else { + transactionToRemove = transaction; + } + } + } + + if (transactionToRemove != null) { + loan.removeLoanTransaction(transactionToRemove); + } + } + } + + private boolean doesLoanChargePaidByContainLoanCharge(Set loanChargePaidBys, LoanCharge loanCharge) { + return loanChargePaidBys.stream() // + .anyMatch(loanChargePaidBy -> loanChargePaidBy.getLoanCharge().equals(loanCharge)); + } + @Override public void processLatestTransaction(final LoanTransaction loanTransaction, final Loan loan) { final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService.processLatestTransaction( diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java index f53cabc10..d0fcf9c70 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java @@ -198,7 +198,7 @@ class LoanChargeWritePlatformServiceImplTest { when(loan.getLoanCharges()).thenReturn(new HashSet<>()); when(loan.getDisbursementDate()).thenReturn(LocalDate.now(ZoneId.systemDefault())); when(loan.getRepaymentScheduleInstallments()).thenReturn(new ArrayList<>()); - when(loan.calculateAmountPercentageAppliedTo(any(LoanCharge.class))).thenReturn(BigDecimal.TEN); + when(loanChargeService.calculateAmountPercentageAppliedTo(any(Loan.class), any(LoanCharge.class))).thenReturn(BigDecimal.TEN); when(loan.fetchNumberOfInstallmensAfterExceptions()).thenReturn(5); when(loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(any(BigDecimal.class))).thenReturn(null); when(loan.deriveSumTotalOfChargesDueAtDisbursement()).thenReturn(BigDecimal.ZERO); @@ -219,8 +219,8 @@ class LoanChargeWritePlatformServiceImplTest { void shouldHandleAccrualBasedOnConfigurationAndDates(boolean isAccrualEnabled, LocalDate businessDate, LocalDate maturityDate, boolean isAccrualExpected) { when(configurationDomainService.isImmediateChargeAccrualPostMaturityEnabled()).thenReturn(isAccrualEnabled); when(loan.getMaturityDate()).thenReturn(maturityDate); - when(loan.handleChargeAppliedTransaction(loanCharge, null)).thenReturn(loanTransaction); - when(loan.createChargeAppliedTransaction(loanCharge, null)).thenReturn(loanTransaction); + when(loanChargeService.handleChargeAppliedTransaction(loan, loanCharge, null)).thenReturn(loanTransaction); + when(loanChargeService.createChargeAppliedTransaction(loan, loanCharge, null)).thenReturn(loanTransaction); if (isAccrualExpected) { when(loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()).thenReturn(true);