FINERACT-2080: Extract complex loan actions out from Loan entity

This commit is contained in:
budaidev
2025-05-12 10:24:26 +02:00
committed by Adam Saghy
parent ec566b7166
commit 788cd7aff7
14 changed files with 537 additions and 501 deletions
@@ -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<Long> {
@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<Long> {
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<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
@@ -699,34 +635,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return lastRepaymentDate;
}
public void removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(final LoanCharge loanCharge) {
if (loanCharge.isDueAtDisbursement()) {
LoanTransaction transactionToRemove = null;
List<LoanTransaction> 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<Long> {
return disbursementDetails;
}
private boolean doesLoanChargePaidByContainLoanCharge(Set<LoanChargePaidBy> 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<LoanRepaymentScheduleInstallment> 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<Long> {
this.transactionProcessingStrategyName = transactionProcessingStrategyName;
}
public void updateLoanCharges(final Set<LoanCharge> loanCharges) {
List<Long> 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<LoanCollateralManagement> loanCollateral) {
if (this.loanCollateralManagements == null) {
this.loanCollateralManagements = new HashSet<>();
@@ -955,49 +746,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
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<Long> {
&& this.loanRepaymentScheduleDetail.isEnableAutoRepaymentForDownPayment();
}
public void handlePayDisbursementTransaction(final Long chargeId, final LoanTransaction chargesPayment,
final List<Long> existingTransactionIds, final List<Long> 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<Long> {
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<LoanRepaymentScheduleInstallment> 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<Long> {
return this.syncDisbursementWithMeeting != null && this.syncDisbursementWithMeeting;
}
public void updateLoanRepaymentScheduleDates(final String recurringRule, final boolean isHolidayEnabled, final List<Holiday> 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<LoanRepaymentScheduleInstallment> 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<Holiday> 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<LoanRepaymentScheduleInstallment> 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<Long> {
return this.charges == null ? new HashSet<>() : this.charges.stream().filter(LoanCharge::isActive).collect(Collectors.toSet());
}
public List<LoanInstallmentCharge> generateInstallmentLoanCharges(final LoanCharge loanCharge) {
final List<LoanInstallmentCharge> loanChargePerInstallments = new ArrayList<>();
if (loanCharge.isInstalmentFee()) {
List<LoanRepaymentScheduleInstallment> 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<Long> fetchAllLoanChargeIds() {
List<Long> list = new ArrayList<>();
for (LoanCharge loanCharge : this.charges) {
list.add(loanCharge.getId());
}
return list;
}
public List<LoanDisbursementDetails> getAllDisbursementDetails() {
return this.disbursementDetails;
}
@@ -2315,26 +1863,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
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<Long> {
return Optional.ofNullable(this.charges).orElse(new HashSet<>());
}
public void addCharge(LoanCharge loanCharge) {
this.getCharges().add(loanCharge);
}
public void removeCharges(Predicate<LoanCharge> predicate) {
charges.removeIf(predicate);
}
@@ -2687,4 +2219,5 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public boolean hasAccelerateChargeOffStrategy() {
return LoanChargeOffBehaviour.ACCELERATE_MATURITY.equals(getLoanProductRelatedDetail().getChargeOffBehaviour());
}
}
@@ -434,7 +434,7 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
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<Long> {
return actualChanges;
}
public static BigDecimal calculatePerInstallmentChargeAmount(final Loan loan, final ChargeCalculationType calculationType,
final BigDecimal percentage) {
Money amount = Money.zero(loan.getCurrency());
List<LoanRepaymentScheduleInstallment> 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<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment installment : installments) {
amount = amount.plus(calculateInstallmentChargeAmount(loan, getChargeCalculation(), getPercentage(), installment));
}
return amount.getAmount();
}
private List<LoanInstallmentCharge> generateInstallmentLoanCharges(final LoanCharge loanCharge) {
Loan loan = this.loan;
final List<LoanInstallmentCharge> loanChargePerInstallments = new ArrayList<>();
if (loanCharge.isInstalmentFee()) {
List<LoanRepaymentScheduleInstallment> 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<LoanInstallmentCharge> remove = new HashSet<>();
final List<LoanInstallmentCharge> newChargeInstallments = this.loan.generateInstallmentLoanCharges(this);
final List<LoanInstallmentCharge> newChargeInstallments = generateInstallmentLoanCharges(this);
if (this.loanInstallmentCharge.isEmpty()) {
this.loanInstallmentCharge.addAll(newChargeInstallments);
} else {
@@ -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<Long> existingTransactionIds,
final List<Long> 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<LoanCharge> loanCharges) {
List<Long> 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<Long> fetchAllLoanChargeIds(final Loan loan) {
List<Long> 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);
}
}
}
}
@@ -101,7 +101,7 @@ public class LoanScheduleService {
final Set<LoanCharge> 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
@@ -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<Holiday> 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<Long> existingTransactionIds, final List<Long> 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<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, boolean isAccountTransfer) {
postJournalEntries(loanAccount, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, false);
@@ -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());
}
}
}
@@ -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
@@ -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
@@ -474,7 +474,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
final Map<String, Object> changes = new LinkedHashMap<>(3);
if (loan.getActiveCharges().contains(loanCharge)) {
final BigDecimal amount = loan.calculateAmountPercentageAppliedTo(loanCharge);
final BigDecimal amount = loanChargeService.calculateAmountPercentageAppliedTo(loan, loanCharge);
final Map<String, Object> 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));
}
@@ -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);
@@ -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<LoanRepaymentScheduleInstallment> 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());
}
}
@@ -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<Holiday> 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<LoanRepaymentScheduleInstallment> 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<Holiday> 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<LoanRepaymentScheduleInstallment> 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<Loan> loansToUpdate;
if (loan.isGroupLoan()) {
@@ -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<LoanTransaction> 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<LoanChargePaidBy> 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(
@@ -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);