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 lombok.Setter;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
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.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator;
|
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.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.organisation.office.domain.Office;
|
import org.apache.fineract.organisation.office.domain.Office;
|
||||||
import org.apache.fineract.organisation.staff.domain.Staff;
|
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.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.Charge;
|
||||||
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
|
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.client.domain.Client;
|
||||||
import org.apache.fineract.portfolio.collateral.domain.LoanCollateral;
|
import org.apache.fineract.portfolio.collateral.domain.LoanCollateral;
|
||||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||||
@@ -265,6 +259,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
@JoinColumn(name = "rescheduledon_userid")
|
@JoinColumn(name = "rescheduledon_userid")
|
||||||
private AppUser rescheduledByUser;
|
private AppUser rescheduledByUser;
|
||||||
|
|
||||||
|
@Setter
|
||||||
@Column(name = "expected_maturedon_date")
|
@Column(name = "expected_maturedon_date")
|
||||||
private LocalDate expectedMaturityDate;
|
private LocalDate expectedMaturityDate;
|
||||||
|
|
||||||
@@ -628,65 +623,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return collateral;
|
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) {
|
public LocalDate getLastRepaymentPeriodDueDate(final boolean includeRecalculatedInterestComponent) {
|
||||||
LocalDate lastRepaymentDate = getDisbursementDate();
|
LocalDate lastRepaymentDate = getDisbursementDate();
|
||||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||||
@@ -699,34 +635,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return lastRepaymentDate;
|
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) {
|
public void removeDisbursementDetails(final long id) {
|
||||||
this.disbursementDetails.remove(fetchLoanDisbursementsById(id));
|
this.disbursementDetails.remove(fetchLoanDisbursementsById(id));
|
||||||
}
|
}
|
||||||
@@ -739,83 +647,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return disbursementDetails;
|
return disbursementDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean doesLoanChargePaidByContainLoanCharge(Set<LoanChargePaidBy> loanChargePaidBys, LoanCharge loanCharge) {
|
public boolean removeLoanTransaction(LoanTransaction transactionToRemove) {
|
||||||
return loanChargePaidBys.stream() //
|
return this.loanTransactions.remove(transactionToRemove);
|
||||||
.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 BigDecimal getTotalInterest() {
|
public BigDecimal getTotalInterest() {
|
||||||
return this.summary.calculateTotalInterestCharged(getRepaymentScheduleInstallments(), getCurrency()).getAmount();
|
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() {
|
public BigDecimal getTotalWrittenOff() {
|
||||||
return this.summary.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() {
|
public Client client() {
|
||||||
return this.client;
|
return this.client;
|
||||||
}
|
}
|
||||||
@@ -850,58 +693,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
this.transactionProcessingStrategyName = transactionProcessingStrategyName;
|
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) {
|
public void updateLoanCollateral(final Set<LoanCollateralManagement> loanCollateral) {
|
||||||
if (this.loanCollateralManagements == null) {
|
if (this.loanCollateralManagements == null) {
|
||||||
this.loanCollateralManagements = new HashSet<>();
|
this.loanCollateralManagements = new HashSet<>();
|
||||||
@@ -955,49 +746,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return this.loanProduct.isMultiDisburseLoan();
|
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) {
|
public BigDecimal calculateOverdueAmountPercentageAppliedTo(final LoanCharge loanCharge, final int penaltyWaitPeriod) {
|
||||||
LoanRepaymentScheduleInstallment installment = loanCharge.getOverdueInstallmentCharge().getInstallment();
|
LoanRepaymentScheduleInstallment installment = loanCharge.getOverdueInstallmentCharge().getInstallment();
|
||||||
LocalDate graceDate = DateUtils.getBusinessLocalDate().minusDays(penaltyWaitPeriod);
|
LocalDate graceDate = DateUtils.getBusinessLocalDate().minusDays(penaltyWaitPeriod);
|
||||||
@@ -1166,26 +914,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
&& this.loanRepaymentScheduleDetail.isEnableAutoRepaymentForDownPayment();
|
&& 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() {
|
public void removePostDatedChecks() {
|
||||||
this.postDatedChecks = new ArrayList<>();
|
this.postDatedChecks = new ArrayList<>();
|
||||||
}
|
}
|
||||||
@@ -1306,31 +1034,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return possibleNextRepaymentDate;
|
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() {
|
public LoanTransaction findWriteOffTransaction() {
|
||||||
return this.loanTransactions.stream() //
|
return this.loanTransactions.stream() //
|
||||||
.filter(transaction -> !transaction.isReversed() && transaction.isWriteOff()) //
|
.filter(transaction -> !transaction.isReversed() && transaction.isWriteOff()) //
|
||||||
@@ -1717,119 +1420,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return this.syncDisbursementWithMeeting != null && this.syncDisbursementWithMeeting;
|
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() {
|
public Group group() {
|
||||||
return this.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());
|
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() {
|
public List<LoanDisbursementDetails> getAllDisbursementDetails() {
|
||||||
return this.disbursementDetails;
|
return this.disbursementDetails;
|
||||||
}
|
}
|
||||||
@@ -2315,26 +1863,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return nextRepaymentDate;
|
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) {
|
public void updateWriteOffReason(CodeValue writeOffReason) {
|
||||||
this.writeOffReason = writeOffReason;
|
this.writeOffReason = writeOffReason;
|
||||||
}
|
}
|
||||||
@@ -2580,6 +2108,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return Optional.ofNullable(this.charges).orElse(new HashSet<>());
|
return Optional.ofNullable(this.charges).orElse(new HashSet<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addCharge(LoanCharge loanCharge) {
|
||||||
|
this.getCharges().add(loanCharge);
|
||||||
|
}
|
||||||
|
|
||||||
public void removeCharges(Predicate<LoanCharge> predicate) {
|
public void removeCharges(Predicate<LoanCharge> predicate) {
|
||||||
charges.removeIf(predicate);
|
charges.removeIf(predicate);
|
||||||
}
|
}
|
||||||
@@ -2687,4 +2219,5 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
public boolean hasAccelerateChargeOffStrategy() {
|
public boolean hasAccelerateChargeOffStrategy() {
|
||||||
return LoanChargeOffBehaviour.ACCELERATE_MATURITY.equals(getLoanProductRelatedDetail().getChargeOffBehaviour());
|
return LoanChargeOffBehaviour.ACCELERATE_MATURITY.equals(getLoanProductRelatedDetail().getChargeOffBehaviour());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-2
@@ -434,7 +434,7 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
this.amountPercentageAppliedTo = amount;
|
this.amountPercentageAppliedTo = amount;
|
||||||
loanCharge = BigDecimal.ZERO;
|
loanCharge = BigDecimal.ZERO;
|
||||||
if (isInstalmentFee()) {
|
if (isInstalmentFee()) {
|
||||||
loanCharge = this.loan.calculatePerInstallmentChargeAmount(ChargeCalculationType.fromInt(this.chargeCalculation),
|
loanCharge = calculatePerInstallmentChargeAmount(loan, ChargeCalculationType.fromInt(this.chargeCalculation),
|
||||||
this.percentage);
|
this.percentage);
|
||||||
}
|
}
|
||||||
if (loanCharge.compareTo(BigDecimal.ZERO) == 0) {
|
if (loanCharge.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
@@ -452,9 +452,67 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return actualChanges;
|
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() {
|
private void updateInstallmentCharges() {
|
||||||
final Collection<LoanInstallmentCharge> remove = new HashSet<>();
|
final Collection<LoanInstallmentCharge> remove = new HashSet<>();
|
||||||
final List<LoanInstallmentCharge> newChargeInstallments = this.loan.generateInstallmentLoanCharges(this);
|
final List<LoanInstallmentCharge> newChargeInstallments = generateInstallmentLoanCharges(this);
|
||||||
if (this.loanInstallmentCharge.isEmpty()) {
|
if (this.loanInstallmentCharge.isEmpty()) {
|
||||||
this.loanInstallmentCharge.addAll(newChargeInstallments);
|
this.loanInstallmentCharge.addAll(newChargeInstallments);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+251
-5
@@ -19,18 +19,27 @@
|
|||||||
package org.apache.fineract.portfolio.loanaccount.service;
|
package org.apache.fineract.portfolio.loanaccount.service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
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.LoanEvent;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
|
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.LoanRepaymentScheduleInstallment;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
|
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.LoanTransaction;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
|
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
@@ -61,23 +70,52 @@ public class LoanChargeService {
|
|||||||
if (loanCharge.isOverdueInstallmentCharge()) {
|
if (loanCharge.isOverdueInstallmentCharge()) {
|
||||||
amount = loan.calculateOverdueAmountPercentageAppliedTo(loanCharge, penaltyWaitPeriod);
|
amount = loan.calculateOverdueAmountPercentageAppliedTo(loanCharge, penaltyWaitPeriod);
|
||||||
} else {
|
} else {
|
||||||
amount = loan.calculateAmountPercentageAppliedTo(loanCharge);
|
amount = calculateAmountPercentageAppliedTo(loan, loanCharge);
|
||||||
}
|
}
|
||||||
chargeAmt = loanCharge.getPercentage();
|
chargeAmt = loanCharge.getPercentage();
|
||||||
if (loanCharge.isInstalmentFee()) {
|
if (loanCharge.isInstalmentFee()) {
|
||||||
totalChargeAmt = loan.calculatePerInstallmentChargeAmount(loanCharge);
|
totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
chargeAmt = loanCharge.amountOrPercentage();
|
chargeAmt = loanCharge.amountOrPercentage();
|
||||||
}
|
}
|
||||||
if (loanCharge.isActive()) {
|
if (loanCharge.isActive()) {
|
||||||
loan.clearLoanInstallmentChargesBeforeRegeneration(loanCharge);
|
clearLoanInstallmentChargesBeforeRegeneration(loanCharge);
|
||||||
loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(),
|
loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(),
|
||||||
totalChargeAmt);
|
totalChargeAmt);
|
||||||
loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate());
|
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,
|
public void makeChargePayment(final Loan loan, final Long chargeId, final List<Long> existingTransactionIds,
|
||||||
final List<Long> existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) {
|
final List<Long> existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) {
|
||||||
loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction);
|
loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction);
|
||||||
@@ -122,16 +160,77 @@ public class LoanChargeService {
|
|||||||
loanLifecycleStateMachine.determineAndTransition(loan, chargesPayment.getTransactionDate());
|
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) {
|
public void addLoanCharge(final Loan loan, final LoanCharge loanCharge) {
|
||||||
loanCharge.update(loan);
|
loanCharge.update(loan);
|
||||||
|
|
||||||
final BigDecimal amount = loan.calculateAmountPercentageAppliedTo(loanCharge);
|
final BigDecimal amount = calculateAmountPercentageAppliedTo(loan, loanCharge);
|
||||||
BigDecimal chargeAmt;
|
BigDecimal chargeAmt;
|
||||||
BigDecimal totalChargeAmt = BigDecimal.ZERO;
|
BigDecimal totalChargeAmt = BigDecimal.ZERO;
|
||||||
if (loanCharge.getChargeCalculation().isPercentageBased()) {
|
if (loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||||
chargeAmt = loanCharge.getPercentage();
|
chargeAmt = loanCharge.getPercentage();
|
||||||
if (loanCharge.isInstalmentFee()) {
|
if (loanCharge.isInstalmentFee()) {
|
||||||
totalChargeAmt = loan.calculatePerInstallmentChargeAmount(loanCharge);
|
totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge);
|
||||||
} else if (loanCharge.isOverdueInstallmentCharge()) {
|
} else if (loanCharge.isOverdueInstallmentCharge()) {
|
||||||
totalChargeAmt = loanCharge.amountOutstanding();
|
totalChargeAmt = loanCharge.amountOutstanding();
|
||||||
}
|
}
|
||||||
@@ -156,4 +255,151 @@ public class LoanChargeService {
|
|||||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, loan);
|
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();
|
final Set<LoanCharge> charges = loan.getActiveCharges();
|
||||||
for (final LoanCharge loanCharge : charges) {
|
for (final LoanCharge loanCharge : charges) {
|
||||||
if (!loanCharge.isDueAtDisbursement()) {
|
if (!loanCharge.isDueAtDisbursement()) {
|
||||||
loan.updateOverdueScheduleInstallment(loanCharge);
|
loanChargeService.updateOverdueScheduleInstallment(loan, loanCharge);
|
||||||
if (loanCharge.getDueLocalDate() == null || (!DateUtils.isBefore(lastRepaymentDate, loanCharge.getDueLocalDate())
|
if (loanCharge.getDueLocalDate() == null || (!DateUtils.isBefore(lastRepaymentDate, loanCharge.getDueLocalDate())
|
||||||
|| loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE))) {
|
|| loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE))) {
|
||||||
if ((loanCharge.isInstalmentFee() || !loanCharge.isWaived()) && (loanCharge.getDueLocalDate() == null
|
if ((loanCharge.isInstalmentFee() || !loanCharge.isWaived()) && (loanCharge.getDueLocalDate() == null
|
||||||
|
|||||||
+21
-1
@@ -397,7 +397,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
|||||||
transactionDate, txnExternalId, loanTransactionType);
|
transactionDate, txnExternalId, loanTransactionType);
|
||||||
|
|
||||||
if (loanTransactionType.isRepaymentAtDisbursement()) {
|
if (loanTransactionType.isRepaymentAtDisbursement()) {
|
||||||
loan.handlePayDisbursementTransaction(chargeId, newPaymentTransaction, existingTransactionIds, existingReversedTransactionIds);
|
handlePayDisbursementTransaction(loan, chargeId, newPaymentTransaction, existingTransactionIds, existingReversedTransactionIds);
|
||||||
} else {
|
} else {
|
||||||
final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled();
|
final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled();
|
||||||
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), transactionDate,
|
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), transactionDate,
|
||||||
@@ -438,6 +438,26 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
|||||||
return newPaymentTransaction;
|
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,
|
private void postJournalEntries(final Loan loanAccount, final List<Long> existingTransactionIds,
|
||||||
final List<Long> existingReversedTransactionIds, boolean isAccountTransfer) {
|
final List<Long> existingReversedTransactionIds, boolean isAccountTransfer) {
|
||||||
postJournalEntries(loanAccount, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, false);
|
postJournalEntries(loanAccount, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, false);
|
||||||
|
|||||||
+3
-2
@@ -112,6 +112,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
|||||||
private final ThreadPoolTaskExecutor taskExecutor;
|
private final ThreadPoolTaskExecutor taskExecutor;
|
||||||
private final TransactionTemplate transactionTemplate;
|
private final TransactionTemplate transactionTemplate;
|
||||||
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
|
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
|
||||||
|
private final LoanChargeService loanChargeService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan
|
* 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;
|
continue;
|
||||||
}
|
}
|
||||||
final BigDecimal chargeAmount = MathUtil.toBigDecimal(chargePortion);
|
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());
|
final LoanChargePaidBy paidBy = new LoanChargePaidBy(transaction, loanCharge, chargeAmount, installment.getInstallmentNumber());
|
||||||
loanCharge.getLoanChargePaidBySet().add(paidBy);
|
loanCharge.getLoanChargePaidBySet().add(paidBy);
|
||||||
transaction.getLoanChargesPaid().add(paidBy);
|
transaction.getLoanChargesPaid().add(paidBy);
|
||||||
@@ -845,7 +846,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
|||||||
Money chargeAmount = loanCharge.getAmount(loan.getCurrency());
|
Money chargeAmount = loanCharge.getAmount(loan.getCurrency());
|
||||||
if (chargeAmount.isNotEqualTo(accrualTransaction.getAmount(loan.getCurrency()))) {
|
if (chargeAmount.isNotEqualTo(accrualTransaction.getAmount(loan.getCurrency()))) {
|
||||||
accrualTransaction.reverse();
|
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";
|
final String chargesParamName = "charges";
|
||||||
if (changes.containsKey(chargesParamName)) {
|
if (changes.containsKey(chargesParamName)) {
|
||||||
loan.updateLoanCharges(possiblyModifiedLoanCharges);
|
loanChargeService.updateLoanCharges(loan, possiblyModifiedLoanCharges);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update installment level delinquency
|
// update installment level delinquency
|
||||||
|
|||||||
+2
-2
@@ -296,8 +296,8 @@ public class LoanChargeAssembler {
|
|||||||
if (percentage == null) {
|
if (percentage == null) {
|
||||||
percentage = chargeDefinition.getAmount();
|
percentage = chargeDefinition.getAmount();
|
||||||
}
|
}
|
||||||
loanCharge = loan.calculatePerInstallmentChargeAmount(ChargeCalculationType.fromInt(chargeDefinition.getChargeCalculation()),
|
loanCharge = LoanCharge.calculatePerInstallmentChargeAmount(loan,
|
||||||
percentage);
|
ChargeCalculationType.fromInt(chargeDefinition.getChargeCalculation()), percentage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If charge type is specified due date and loan is multi disburment
|
// 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);
|
final Map<String, Object> changes = new LinkedHashMap<>(3);
|
||||||
if (loan.getActiveCharges().contains(loanCharge)) {
|
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);
|
final Map<String, Object> loanChargeChanges = loanCharge.update(command, amount);
|
||||||
changes.putAll(loanChargeChanges);
|
changes.putAll(loanChargeChanges);
|
||||||
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
|
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,
|
// the loan product uses Upfront Accruals, or only when the loan are closed too,
|
||||||
if ((loan.getStatus().isActive() && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct())
|
if ((loan.getStatus().isActive() && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct())
|
||||||
|| loan.getStatus().isOverpaid() || loan.getStatus().isClosedObligationsMet()) {
|
|| loan.getStatus().isOverpaid() || loan.getStatus().isClosedObligationsMet()) {
|
||||||
final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null);
|
final LoanTransaction applyLoanChargeTransaction = loanChargeService.handleChargeAppliedTransaction(loan, loanCharge, null);
|
||||||
if (applyLoanChargeTransaction != null) {
|
if (applyLoanChargeTransaction != null) {
|
||||||
this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction);
|
this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction);
|
||||||
businessEventNotifierService
|
businessEventNotifierService
|
||||||
@@ -1123,7 +1123,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
|||||||
}
|
}
|
||||||
} else if (configurationDomainService.isImmediateChargeAccrualPostMaturityEnabled()
|
} else if (configurationDomainService.isImmediateChargeAccrualPostMaturityEnabled()
|
||||||
&& DateUtils.getBusinessLocalDate().isAfter(loan.getMaturityDate())) {
|
&& DateUtils.getBusinessLocalDate().isAfter(loan.getMaturityDate())) {
|
||||||
final LoanTransaction loanTransaction = loan.createChargeAppliedTransaction(loanCharge, null);
|
final LoanTransaction loanTransaction = loanChargeService.createChargeAppliedTransaction(loan, loanCharge, null);
|
||||||
this.loanTransactionRepository.saveAndFlush(loanTransaction);
|
this.loanTransactionRepository.saveAndFlush(loanTransaction);
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(loanTransaction));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(loanTransaction));
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -204,7 +204,7 @@ public class LoanDisbursementService {
|
|||||||
}
|
}
|
||||||
} else if (disbursedOn.equals(loan.getActualDisbursementDate())
|
} else if (disbursedOn.equals(loan.getActualDisbursementDate())
|
||||||
&& loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) {
|
&& loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) {
|
||||||
loan.handleChargeAppliedTransaction(charge, disbursedOn);
|
loanChargeService.handleChargeAppliedTransaction(loan, charge, disbursedOn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +273,7 @@ public class LoanDisbursementService {
|
|||||||
} else {
|
} else {
|
||||||
if (!loanChargeIds.isEmpty() && loanChargeIds.size() != chargeIdLength) {
|
if (!loanChargeIds.isEmpty() && loanChargeIds.size() != chargeIdLength) {
|
||||||
for (Long chargeId : loanChargeIds) {
|
for (Long chargeId : loanChargeIds) {
|
||||||
final LoanCharge deleteCharge = loan.fetchLoanChargesById(chargeId);
|
final LoanCharge deleteCharge = loanChargeService.fetchLoanChargesById(loan, chargeId);
|
||||||
if (loan.getCharges().contains(deleteCharge)) {
|
if (loan.getCharges().contains(deleteCharge)) {
|
||||||
loanChargeValidator.validateLoanIsNotClosed(loan, deleteCharge);
|
loanChargeValidator.validateLoanIsNotClosed(loan, deleteCharge);
|
||||||
loanChargeValidator.validateLoanChargeIsNotWaived(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 ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
|
||||||
final CurrencyData currencyData = applicationCurrency.toData();
|
final CurrencyData currencyData = applicationCurrency.toData();
|
||||||
|
|
||||||
final LoanTransaction waiveOfInterest = loan.deriveDefaultInterestWaiverTransaction();
|
final LoanTransaction waiveOfInterest = deriveDefaultInterestWaiverTransaction(loan);
|
||||||
|
|
||||||
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.WAIVE_INTEREST);
|
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.WAIVE_INTEREST);
|
||||||
|
|
||||||
@@ -2235,4 +2235,29 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
public boolean existsByLoanId(Long loanId) {
|
public boolean existsByLoanId(Long loanId) {
|
||||||
return loanRepositoryWrapper.existsByLoanId(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.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.organisation.holiday.domain.Holiday;
|
import org.apache.fineract.organisation.holiday.domain.Holiday;
|
||||||
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
|
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.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.organisation.office.domain.Office;
|
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.CalendarRepository;
|
||||||
import org.apache.fineract.portfolio.calendar.domain.CalendarType;
|
import org.apache.fineract.portfolio.calendar.domain.CalendarType;
|
||||||
import org.apache.fineract.portfolio.calendar.exception.CalendarParameterUpdateNotSupportedException;
|
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.domain.Client;
|
||||||
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
|
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
|
||||||
import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand;
|
import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand;
|
||||||
import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand;
|
import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand;
|
||||||
import org.apache.fineract.portfolio.collectionsheet.command.SingleDisbursalCommand;
|
import org.apache.fineract.portfolio.collectionsheet.command.SingleDisbursalCommand;
|
||||||
import org.apache.fineract.portfolio.collectionsheet.command.SingleRepaymentCommand;
|
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.domain.Group;
|
||||||
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
||||||
@@ -2003,10 +2006,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
existingReversedTransactionIds);
|
existingReversedTransactionIds);
|
||||||
createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO);
|
createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO);
|
||||||
} else if (rescheduleBasedOnMeetingDates != null && rescheduleBasedOnMeetingDates) {
|
} else if (rescheduleBasedOnMeetingDates != null && rescheduleBasedOnMeetingDates) {
|
||||||
loan.updateLoanRepaymentScheduleDates(calendar.getRecurrence(), isHolidayEnabled, holidays, workingDays,
|
updateLoanRepaymentScheduleDates(loan, calendar.getRecurrence(), isHolidayEnabled, holidays, workingDays,
|
||||||
presentMeetingDate, newMeetingDate, isSkipRepaymentOnFirstMonth, numberOfDays);
|
presentMeetingDate, newMeetingDate, isSkipRepaymentOnFirstMonth, numberOfDays);
|
||||||
} else {
|
} else {
|
||||||
loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled,
|
updateLoanRepaymentScheduleDates(loan, calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled,
|
||||||
holidays, workingDays, isSkipRepaymentOnFirstMonth, numberOfDays);
|
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) {
|
private void removeLoanCycle(final Loan loan) {
|
||||||
final List<Loan> loansToUpdate;
|
final List<Loan> loansToUpdate;
|
||||||
if (loan.isGroupLoan()) {
|
if (loan.isGroupLoan()) {
|
||||||
|
|||||||
+38
-1
@@ -21,14 +21,18 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
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.interestpauses.service.LoanAccountTransfersService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
|
import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountService;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
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.LoanRepaymentScheduleProcessingWrapper;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
@@ -83,7 +87,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
|
|||||||
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
|
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
|
||||||
}
|
}
|
||||||
|
|
||||||
loan.removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(loanCharge);
|
removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(loan, loanCharge);
|
||||||
|
|
||||||
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loan.getCurrency())) {
|
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loan.getCurrency())) {
|
||||||
/*
|
/*
|
||||||
@@ -100,6 +104,39 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
|
|||||||
loan.updateLoanSummaryDerivedFields();
|
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
|
@Override
|
||||||
public void processLatestTransaction(final LoanTransaction loanTransaction, final Loan loan) {
|
public void processLatestTransaction(final LoanTransaction loanTransaction, final Loan loan) {
|
||||||
final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService.processLatestTransaction(
|
final ChangedTransactionDetail changedTransactionDetail = loanTransactionProcessingService.processLatestTransaction(
|
||||||
|
|||||||
+3
-3
@@ -198,7 +198,7 @@ class LoanChargeWritePlatformServiceImplTest {
|
|||||||
when(loan.getLoanCharges()).thenReturn(new HashSet<>());
|
when(loan.getLoanCharges()).thenReturn(new HashSet<>());
|
||||||
when(loan.getDisbursementDate()).thenReturn(LocalDate.now(ZoneId.systemDefault()));
|
when(loan.getDisbursementDate()).thenReturn(LocalDate.now(ZoneId.systemDefault()));
|
||||||
when(loan.getRepaymentScheduleInstallments()).thenReturn(new ArrayList<>());
|
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.fetchNumberOfInstallmensAfterExceptions()).thenReturn(5);
|
||||||
when(loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(any(BigDecimal.class))).thenReturn(null);
|
when(loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(any(BigDecimal.class))).thenReturn(null);
|
||||||
when(loan.deriveSumTotalOfChargesDueAtDisbursement()).thenReturn(BigDecimal.ZERO);
|
when(loan.deriveSumTotalOfChargesDueAtDisbursement()).thenReturn(BigDecimal.ZERO);
|
||||||
@@ -219,8 +219,8 @@ class LoanChargeWritePlatformServiceImplTest {
|
|||||||
void shouldHandleAccrualBasedOnConfigurationAndDates(boolean isAccrualEnabled, LocalDate businessDate, LocalDate maturityDate, boolean isAccrualExpected) {
|
void shouldHandleAccrualBasedOnConfigurationAndDates(boolean isAccrualEnabled, LocalDate businessDate, LocalDate maturityDate, boolean isAccrualExpected) {
|
||||||
when(configurationDomainService.isImmediateChargeAccrualPostMaturityEnabled()).thenReturn(isAccrualEnabled);
|
when(configurationDomainService.isImmediateChargeAccrualPostMaturityEnabled()).thenReturn(isAccrualEnabled);
|
||||||
when(loan.getMaturityDate()).thenReturn(maturityDate);
|
when(loan.getMaturityDate()).thenReturn(maturityDate);
|
||||||
when(loan.handleChargeAppliedTransaction(loanCharge, null)).thenReturn(loanTransaction);
|
when(loanChargeService.handleChargeAppliedTransaction(loan, loanCharge, null)).thenReturn(loanTransaction);
|
||||||
when(loan.createChargeAppliedTransaction(loanCharge, null)).thenReturn(loanTransaction);
|
when(loanChargeService.createChargeAppliedTransaction(loan, loanCharge, null)).thenReturn(loanTransaction);
|
||||||
|
|
||||||
if (isAccrualExpected) {
|
if (isAccrualExpected) {
|
||||||
when(loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()).thenReturn(true);
|
when(loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()).thenReturn(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user