FINERACT-2080: Extract complex loan actions out from Loan entity
This commit is contained in:
+8
-475
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+60
-2
@@ -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 {
|
||||
|
||||
+251
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+21
-1
@@ -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);
|
||||
|
||||
+3
-2
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+3
-3
@@ -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));
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+26
-1
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+118
-2
@@ -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()) {
|
||||
|
||||
+38
-1
@@ -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(
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user