FINERACT-1968: Support horizontal and vertical installment processing
This commit is contained in:
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Sort loan charges by effective due date
|
||||
*
|
||||
* Null values go to the end
|
||||
*/
|
||||
public final class LoanChargeEffectiveDueDateComparator implements Comparator<LoanCharge> {
|
||||
|
||||
public static final LoanChargeEffectiveDueDateComparator INSTANCE = new LoanChargeEffectiveDueDateComparator();
|
||||
|
||||
private LoanChargeEffectiveDueDateComparator() {}
|
||||
|
||||
@Override
|
||||
public int compare(final LoanCharge o1, final LoanCharge o2) {
|
||||
if (o1.getEffectiveDueDate() == null && o2.getEffectiveDueDate() != null) {
|
||||
return 1;
|
||||
} else if (o1.getEffectiveDueDate() == null) {
|
||||
return 0;
|
||||
} else if (o2.getEffectiveDueDate() == null) {
|
||||
return -1;
|
||||
}
|
||||
return o1.getEffectiveDueDate().compareTo(o2.getEffectiveDueDate());
|
||||
}
|
||||
}
|
||||
+23
@@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
|
||||
|
||||
@Entity
|
||||
@@ -424,6 +425,24 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
||||
this.penaltyCharges = null;
|
||||
}
|
||||
|
||||
public interface PaymentFunction {
|
||||
|
||||
Money accept(LocalDate transactionDate, Money transactionAmountRemaining);
|
||||
}
|
||||
|
||||
public PaymentFunction getPaymentFunction(AllocationType allocationType, PaymentAction action) {
|
||||
return switch (allocationType) {
|
||||
case PENALTY -> PaymentAction.PAY.equals(action) ? this::payPenaltyChargesComponent
|
||||
: PaymentAction.UNPAY.equals(action) ? this::unpayPenaltyChargesComponent : null;
|
||||
case FEE -> PaymentAction.PAY.equals(action) ? this::payFeeChargesComponent
|
||||
: PaymentAction.UNPAY.equals(action) ? this::unpayFeeChargesComponent : null;
|
||||
case INTEREST -> PaymentAction.PAY.equals(action) ? this::payInterestComponent
|
||||
: PaymentAction.UNPAY.equals(action) ? this::unpayInterestComponent : null;
|
||||
case PRINCIPAL -> PaymentAction.PAY.equals(action) ? this::payPrincipalComponent
|
||||
: PaymentAction.UNPAY.equals(action) ? this::unpayPrincipalComponent : null;
|
||||
};
|
||||
}
|
||||
|
||||
public Money payPenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
|
||||
|
||||
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
|
||||
@@ -967,4 +986,8 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
||||
public void resetPrincipalDue() {
|
||||
this.principal = null;
|
||||
}
|
||||
|
||||
public enum PaymentAction {
|
||||
PAY, UNPAY
|
||||
}
|
||||
}
|
||||
|
||||
+822
@@ -0,0 +1,822 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
/**
|
||||
* Abstract implementation of {@link LoanRepaymentScheduleTransactionProcessor} which is more convenient for concrete
|
||||
* implementations to extend.
|
||||
*
|
||||
* @see InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor
|
||||
*
|
||||
* @see HeavensFamilyLoanRepaymentScheduleTransactionProcessor
|
||||
* @see CreocoreLoanRepaymentScheduleTransactionProcessor
|
||||
*/
|
||||
public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
@Override
|
||||
public boolean accept(String s) {
|
||||
return getCode().equalsIgnoreCase(s) || getName().equalsIgnoreCase(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbursementDate,
|
||||
final List<LoanTransaction> transactionsPostDisbursement, final MonetaryCurrency currency,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) {
|
||||
|
||||
if (charges != null) {
|
||||
for (final LoanCharge loanCharge : charges) {
|
||||
if (!loanCharge.isDueAtDisbursement()) {
|
||||
loanCharge.resetPaidAmount(currency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
currentInstallment.resetDerivedComponents();
|
||||
currentInstallment.updateDerivedFields(currency, disbursementDate);
|
||||
}
|
||||
|
||||
// re-process loan charges over repayment periods (picking up on waived
|
||||
// loan charges)
|
||||
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
|
||||
wrapper.reprocess(currency, disbursementDate, installments, charges);
|
||||
|
||||
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
|
||||
final List<LoanTransaction> transactionsToBeProcessed = new ArrayList<>();
|
||||
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
|
||||
if (loanTransaction.isChargePayment()) {
|
||||
List<LoanChargePaidDetail> chargePaidDetails = new ArrayList<>();
|
||||
final Set<LoanChargePaidBy> chargePaidBies = loanTransaction.getLoanChargesPaid();
|
||||
final Set<LoanCharge> transferCharges = new HashSet<>();
|
||||
for (final LoanChargePaidBy chargePaidBy : chargePaidBies) {
|
||||
LoanCharge loanCharge = chargePaidBy.getLoanCharge();
|
||||
transferCharges.add(loanCharge);
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
chargePaidDetails.addAll(loanCharge.fetchRepaymentInstallment(currency));
|
||||
}
|
||||
}
|
||||
LocalDate startDate = disbursementDate;
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
for (final LoanCharge loanCharge : transferCharges) {
|
||||
boolean isDue = installment.isFirstPeriod()
|
||||
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(startDate, installment.getDueDate())
|
||||
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate());
|
||||
if (isDue) {
|
||||
Money amountForProcess = loanCharge.getAmount(currency);
|
||||
if (amountForProcess.isGreaterThan(loanTransaction.getAmount(currency))) {
|
||||
amountForProcess = loanTransaction.getAmount(currency);
|
||||
}
|
||||
LoanChargePaidDetail chargePaidDetail = new LoanChargePaidDetail(amountForProcess, installment,
|
||||
loanCharge.isFeeCharge());
|
||||
chargePaidDetails.add(chargePaidDetail);
|
||||
break;
|
||||
}
|
||||
}
|
||||
startDate = installment.getDueDate();
|
||||
}
|
||||
loanTransaction.resetDerivedComponents();
|
||||
Money unprocessed = loanTransaction.getAmount(currency);
|
||||
for (LoanChargePaidDetail chargePaidDetail : chargePaidDetails) {
|
||||
final List<LoanRepaymentScheduleInstallment> processInstallments = new ArrayList<>(1);
|
||||
processInstallments.add(chargePaidDetail.getInstallment());
|
||||
Money processAmt = chargePaidDetail.getAmount();
|
||||
if (processAmt.isGreaterThan(unprocessed)) {
|
||||
processAmt = unprocessed;
|
||||
}
|
||||
unprocessed = handleTransactionAndCharges(loanTransaction, currency, processInstallments, transferCharges, processAmt,
|
||||
chargePaidDetail.isFeeCharge());
|
||||
if (!unprocessed.isGreaterThanZero()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (unprocessed.isGreaterThanZero()) {
|
||||
onLoanOverpayment(loanTransaction, unprocessed);
|
||||
loanTransaction.updateOverPayments(unprocessed);
|
||||
}
|
||||
|
||||
} else {
|
||||
transactionsToBeProcessed.add(loanTransaction);
|
||||
}
|
||||
}
|
||||
|
||||
for (final LoanTransaction loanTransaction : transactionsToBeProcessed) {
|
||||
// TODO: analyze and remove this
|
||||
if (!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) {
|
||||
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator
|
||||
.comparing(LoanRepaymentScheduleInstallment::getDueDate);
|
||||
installments.sort(byDate);
|
||||
}
|
||||
|
||||
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
|
||||
// pass through for new transactions
|
||||
if (loanTransaction.getId() == null) {
|
||||
processLatestTransaction(loanTransaction, currency, installments, charges, null);
|
||||
loanTransaction.adjustInterestComponent(currency);
|
||||
} else {
|
||||
/**
|
||||
* For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties)
|
||||
* has changed.<br>
|
||||
**/
|
||||
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
|
||||
|
||||
// Reset derived component of new loan transaction and
|
||||
// re-process transaction
|
||||
processLatestTransaction(newLoanTransaction, currency, installments, charges, null);
|
||||
newLoanTransaction.adjustInterestComponent(currency);
|
||||
/**
|
||||
* Check if the transaction amounts have changed. If so, reverse the original transaction and update
|
||||
* changedTransactionDetail accordingly
|
||||
**/
|
||||
if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
|
||||
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
|
||||
} else {
|
||||
createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (loanTransaction.isWriteOff()) {
|
||||
loanTransaction.resetDerivedComponents();
|
||||
handleWriteOff(loanTransaction, currency, installments);
|
||||
} else if (loanTransaction.isRefundForActiveLoan()) {
|
||||
loanTransaction.resetDerivedComponents();
|
||||
handleRefund(loanTransaction, currency, installments, charges);
|
||||
} else if (loanTransaction.isCreditBalanceRefund()) {
|
||||
recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed);
|
||||
} else if (loanTransaction.isChargeback()) {
|
||||
recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed);
|
||||
reprocessChargebackTransactionRelation(changedTransactionDetail, transactionsToBeProcessed);
|
||||
} else if (loanTransaction.isChargeOff()) {
|
||||
recalculateChargeOffTransaction(changedTransactionDetail, loanTransaction, currency, installments);
|
||||
}
|
||||
}
|
||||
reprocessInstallments(installments, currency);
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processLatestTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, Money overpaidAmount) {
|
||||
switch (loanTransaction.getTypeOf()) {
|
||||
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
|
||||
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
|
||||
case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments);
|
||||
default -> {
|
||||
Money transactionAmountUnprocessed = handleTransactionAndCharges(loanTransaction, currency, installments, charges, null,
|
||||
false);
|
||||
if (transactionAmountUnprocessed.isGreaterThanZero()) {
|
||||
if (loanTransaction.isWaiver()) {
|
||||
loanTransaction.updateComponentsAndTotal(transactionAmountUnprocessed.zero(), transactionAmountUnprocessed.zero(),
|
||||
transactionAmountUnprocessed.zero(), transactionAmountUnprocessed.zero());
|
||||
} else {
|
||||
onLoanOverpayment(loanTransaction, transactionAmountUnprocessed);
|
||||
loanTransaction.updateOverPayments(transactionAmountUnprocessed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money handleRepaymentSchedule(final List<LoanTransaction> transactionsPostDisbursement, final MonetaryCurrency currency,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> loanCharges) {
|
||||
Money unProcessed = Money.zero(currency);
|
||||
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
|
||||
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
|
||||
loanTransaction.resetDerivedComponents();
|
||||
}
|
||||
if (loanTransaction.isInterestWaiver()) {
|
||||
processTransaction(loanTransaction, currency, installments, loanCharges, null);
|
||||
} else {
|
||||
unProcessed = processTransaction(loanTransaction, currency, installments, loanCharges, null);
|
||||
}
|
||||
}
|
||||
return unProcessed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInterestFirstRepaymentScheduleTransactionProcessor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// abstract interface
|
||||
|
||||
/**
|
||||
* For early/'in advance' repayments.
|
||||
*
|
||||
* @param transactionMappings
|
||||
* TODO
|
||||
* @param charges
|
||||
*/
|
||||
protected abstract Money handleTransactionThatIsPaymentInAdvanceOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
|
||||
List<LoanRepaymentScheduleInstallment> installments, LoanTransaction loanTransaction, Money paymentInAdvance,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges);
|
||||
|
||||
/**
|
||||
* For normal on-time repayments.
|
||||
*
|
||||
* @param transactionMappings
|
||||
* TODO
|
||||
* @param charges
|
||||
*/
|
||||
protected abstract Money handleTransactionThatIsOnTimePaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
|
||||
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges);
|
||||
|
||||
/**
|
||||
* For late repayments, how should components of installment be paid off
|
||||
*
|
||||
* @param transactionMappings
|
||||
* TODO
|
||||
* @param charges
|
||||
*/
|
||||
protected abstract Money handleTransactionThatIsALateRepaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
|
||||
List<LoanRepaymentScheduleInstallment> installments, LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges);
|
||||
|
||||
/**
|
||||
* Invoked when a transaction results in an over-payment of the full loan.
|
||||
*
|
||||
* transaction amount is greater than the total expected principal and interest of the loan.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {
|
||||
// empty implementation by default.
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a there is a refund of an active loan or undo of an active loan
|
||||
*
|
||||
* Undoes principal, interest, fees and charges of this transaction based on the repayment strategy
|
||||
*
|
||||
* @param transactionMappings
|
||||
* TODO
|
||||
*
|
||||
*/
|
||||
protected abstract Money handleRefundTransactionPaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
|
||||
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings);
|
||||
|
||||
/**
|
||||
* This method is responsible for checking if the current transaction is 'an advance/early payment' based on the
|
||||
* details passed through.
|
||||
*
|
||||
* Default implementation is check transaction date is before installment due date.
|
||||
*/
|
||||
protected boolean isTransactionInAdvanceOfInstallment(final int installmentIndex,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate) {
|
||||
final LoanRepaymentScheduleInstallment currentInstallment = installments.get(installmentIndex);
|
||||
return DateUtils.isBefore(transactionDate, currentInstallment.getDueDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is responsible for checking if the current transaction is 'an advance/early payment' based on the
|
||||
* details passed through.
|
||||
*
|
||||
* Default implementation simply processes transactions as 'Late' if the transaction date is after the installment
|
||||
* due date.
|
||||
*/
|
||||
protected boolean isTransactionALateRepaymentOnInstallment(final int installmentIndex,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate) {
|
||||
final LoanRepaymentScheduleInstallment currentInstallment = installments.get(installmentIndex);
|
||||
return DateUtils.isAfter(transactionDate, currentInstallment.getDueDate());
|
||||
}
|
||||
|
||||
private void recalculateChargeOffTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction,
|
||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments) {
|
||||
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
|
||||
newLoanTransaction.resetDerivedComponents();
|
||||
// determine how much is outstanding total and breakdown for principal, interest and charges
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltychargesPortion = Money.zero(currency);
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
if (currentInstallment.isNotFullyPaidOff()) {
|
||||
principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(currency));
|
||||
interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(currency));
|
||||
feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesOutstanding(currency));
|
||||
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesCharged(currency));
|
||||
}
|
||||
}
|
||||
|
||||
newLoanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
|
||||
if (!LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
|
||||
createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail);
|
||||
}
|
||||
}
|
||||
|
||||
private void reprocessChargebackTransactionRelation(ChangedTransactionDetail changedTransactionDetail,
|
||||
List<LoanTransaction> transactionsToBeProcessed) {
|
||||
|
||||
List<LoanTransaction> mergedTransactionList = getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail);
|
||||
for (Map.Entry<Long, LoanTransaction> entry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
if (entry.getValue().isChargeback()) {
|
||||
for (LoanTransaction loanTransaction : mergedTransactionList) {
|
||||
if (loanTransaction.isReversed()) {
|
||||
continue;
|
||||
}
|
||||
LoanTransactionRelation newLoanTransactionRelation = null;
|
||||
LoanTransactionRelation oldLoanTransactionRelation = null;
|
||||
for (LoanTransactionRelation transactionRelation : loanTransaction.getLoanTransactionRelations()) {
|
||||
if (LoanTransactionRelationTypeEnum.CHARGEBACK.equals(transactionRelation.getRelationType())
|
||||
&& entry.getKey().equals(transactionRelation.getToTransaction().getId())) {
|
||||
newLoanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, entry.getValue(),
|
||||
LoanTransactionRelationTypeEnum.CHARGEBACK);
|
||||
oldLoanTransactionRelation = transactionRelation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newLoanTransactionRelation != null) {
|
||||
loanTransaction.getLoanTransactionRelations().add(newLoanTransactionRelation);
|
||||
loanTransaction.getLoanTransactionRelations().remove(oldLoanTransactionRelation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void reprocessInstallments(List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency) {
|
||||
LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1);
|
||||
if (lastInstallment.isAdditional() && lastInstallment.getDue(currency).isZero()) {
|
||||
installments.remove(lastInstallment);
|
||||
}
|
||||
// TODO: rewrite and handle it at the proper place when disbursement handling got fixed
|
||||
for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) {
|
||||
if (loanRepaymentScheduleInstallment.getTotalOutstanding(currency).isGreaterThanZero()) {
|
||||
loanRepaymentScheduleInstallment.updateObligationMet(false);
|
||||
loanRepaymentScheduleInstallment.updateObligationMetOnDate(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void recalculateCreditTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction,
|
||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
||||
List<LoanTransaction> transactionsToBeProcessed) {
|
||||
// pass through for new transactions
|
||||
if (loanTransaction.getId() == null) {
|
||||
return;
|
||||
}
|
||||
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
|
||||
|
||||
List<LoanTransaction> mergedList = getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail);
|
||||
Money overpaidAmount = calculateOverpaidAmount(loanTransaction, mergedList, installments, currency);
|
||||
processCreditTransaction(newLoanTransaction, overpaidAmount, currency, installments);
|
||||
if (!LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
|
||||
createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail);
|
||||
}
|
||||
}
|
||||
|
||||
private List<LoanTransaction> getMergedTransactionList(List<LoanTransaction> transactionList,
|
||||
ChangedTransactionDetail changedTransactionDetail) {
|
||||
List<LoanTransaction> mergedList = new ArrayList<>(changedTransactionDetail.getNewTransactionMappings().values());
|
||||
mergedList.addAll(new ArrayList<>(transactionList));
|
||||
return mergedList;
|
||||
}
|
||||
|
||||
protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction,
|
||||
ChangedTransactionDetail changedTransactionDetail) {
|
||||
loanTransaction.reverse();
|
||||
loanTransaction.updateExternalId(null);
|
||||
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
|
||||
// Adding Replayed relation from newly created transaction to reversed transaction
|
||||
newLoanTransaction.getLoanTransactionRelations().add(
|
||||
LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
|
||||
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction);
|
||||
|
||||
}
|
||||
|
||||
private Money calculateOverpaidAmount(LoanTransaction loanTransaction, List<LoanTransaction> transactions,
|
||||
List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency) {
|
||||
Money totalPaidInRepayments = Money.zero(currency);
|
||||
|
||||
Money cumulativeTotalPaidOnInstallments = Money.zero(currency);
|
||||
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
|
||||
cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments
|
||||
.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestPaid(currency)))
|
||||
.plus(scheduledRepayment.getFeeChargesPaid(currency)).plus(scheduledRepayment.getPenaltyChargesPaid(currency));
|
||||
}
|
||||
|
||||
for (final LoanTransaction transaction : transactions) {
|
||||
if (transaction.isReversed()) {
|
||||
continue;
|
||||
}
|
||||
if (transaction.equals(loanTransaction)) {
|
||||
// We want to process only the transactions prior to the actual one
|
||||
break;
|
||||
}
|
||||
if (transaction.isRefund() || transaction.isRefundForActiveLoan()) {
|
||||
totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getAmount(currency));
|
||||
} else if (transaction.isCreditBalanceRefund() || transaction.isChargeback()) {
|
||||
totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getOverPaymentPortion(currency));
|
||||
} else if (transaction.isRepaymentLikeType()) {
|
||||
totalPaidInRepayments = totalPaidInRepayments.plus(transaction.getAmount(currency));
|
||||
}
|
||||
}
|
||||
|
||||
// if total paid in transactions higher than repayment schedule then
|
||||
// theres an overpayment.
|
||||
return MathUtil.negativeToZero(totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments));
|
||||
}
|
||||
|
||||
private void processCreditTransaction(LoanTransaction loanTransaction, Money overpaidAmount, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments) {
|
||||
loanTransaction.resetDerivedComponents();
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
|
||||
installments.sort(byDate);
|
||||
final Money zeroMoney = Money.zero(currency);
|
||||
Money transactionAmount = loanTransaction.getAmount(currency);
|
||||
Money principalPortion = MathUtil.negativeToZero(loanTransaction.getAmount(currency).minus(overpaidAmount));
|
||||
Money repaidAmount = MathUtil.negativeToZero(transactionAmount.minus(principalPortion));
|
||||
loanTransaction.updateOverPayments(repaidAmount);
|
||||
loanTransaction.updateComponents(principalPortion, zeroMoney, zeroMoney, zeroMoney);
|
||||
|
||||
if (principalPortion.isGreaterThanZero()) {
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
boolean loanTransactionMapped = false;
|
||||
LocalDate pastDueDate = null;
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
pastDueDate = currentInstallment.getDueDate();
|
||||
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
|
||||
currentInstallment.addToCredits(transactionAmount.getAmount());
|
||||
currentInstallment.addToPrincipal(transactionDate, transactionAmount);
|
||||
if (repaidAmount.isGreaterThanZero()) {
|
||||
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
repaidAmount, zeroMoney, zeroMoney, zeroMoney));
|
||||
}
|
||||
loanTransactionMapped = true;
|
||||
break;
|
||||
|
||||
// If already exists an additional installment just update the due date and
|
||||
// principal from the Loan chargeback / CBR transaction
|
||||
} else if (currentInstallment.isAdditional()) {
|
||||
if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) {
|
||||
currentInstallment.updateDueDate(transactionDate);
|
||||
}
|
||||
|
||||
currentInstallment.updateCredits(transactionDate, transactionAmount);
|
||||
if (repaidAmount.isGreaterThanZero()) {
|
||||
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
repaidAmount, zeroMoney, zeroMoney, zeroMoney));
|
||||
}
|
||||
loanTransactionMapped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// New installment will be added (N+1 scenario)
|
||||
if (!loanTransactionMapped) {
|
||||
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
|
||||
LoanRepaymentScheduleInstallment currentInstallment = installments.get(installments.size() - 1);
|
||||
currentInstallment.addToCredits(transactionAmount.getAmount());
|
||||
currentInstallment.addToPrincipal(transactionDate, transactionAmount);
|
||||
if (repaidAmount.isGreaterThanZero()) {
|
||||
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
repaidAmount, zeroMoney, zeroMoney, zeroMoney));
|
||||
}
|
||||
} else {
|
||||
Loan loan = loanTransaction.getLoan();
|
||||
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, (installments.size() + 1),
|
||||
pastDueDate, transactionDate, transactionAmount.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(),
|
||||
zeroMoney.getAmount(), false, null);
|
||||
installment.markAsAdditional();
|
||||
installment.addToCredits(transactionAmount.getAmount());
|
||||
loan.addLoanRepaymentScheduleInstallment(installment);
|
||||
|
||||
if (repaidAmount.isGreaterThanZero()) {
|
||||
installment.payPrincipalComponent(loanTransaction.getTransactionDate(), repaidAmount);
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, installment,
|
||||
repaidAmount, zeroMoney, zeroMoney, zeroMoney));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
}
|
||||
}
|
||||
|
||||
protected Money handleTransactionAndCharges(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, final Money chargeAmountToProcess,
|
||||
final boolean isFeeCharge) {
|
||||
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
|
||||
loanTransaction.resetDerivedComponents();
|
||||
}
|
||||
Money transactionAmountUnprocessed = processTransaction(loanTransaction, currency, installments, charges, chargeAmountToProcess);
|
||||
|
||||
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
|
||||
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
|
||||
Integer installmentNumber = null;
|
||||
if (loanTransaction.isChargePayment() && installments.size() == 1) {
|
||||
installmentNumber = installments.get(0).getInstallmentNumber();
|
||||
}
|
||||
|
||||
if (loanTransaction.isNotWaiver() && !loanTransaction.isAccrual()) {
|
||||
Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
|
||||
Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
|
||||
if (chargeAmountToProcess != null && feeCharges.isGreaterThan(chargeAmountToProcess)) {
|
||||
if (isFeeCharge) {
|
||||
feeCharges = chargeAmountToProcess;
|
||||
} else {
|
||||
penaltyCharges = chargeAmountToProcess;
|
||||
}
|
||||
}
|
||||
if (feeCharges.isGreaterThanZero()) {
|
||||
updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, installmentNumber);
|
||||
}
|
||||
|
||||
if (penaltyCharges.isGreaterThanZero()) {
|
||||
updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, installmentNumber);
|
||||
}
|
||||
}
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
protected Money processTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, Money amountToProcess) {
|
||||
int installmentIndex = 0;
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
||||
if (amountToProcess != null) {
|
||||
transactionAmountUnprocessed = amountToProcess;
|
||||
}
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
if (transactionAmountUnprocessed.isGreaterThanZero()) {
|
||||
if (currentInstallment.isNotFullyPaidOff()) {
|
||||
if (isTransactionInAdvanceOfInstallment(installmentIndex, installments, transactionDate)) {
|
||||
transactionAmountUnprocessed = handleTransactionThatIsPaymentInAdvanceOfInstallment(currentInstallment,
|
||||
installments, loanTransaction, transactionAmountUnprocessed, transactionMappings, charges);
|
||||
} else if (isTransactionALateRepaymentOnInstallment(installmentIndex, installments, transactionDate)) {
|
||||
transactionAmountUnprocessed = handleTransactionThatIsALateRepaymentOfInstallment(currentInstallment, installments,
|
||||
loanTransaction, transactionAmountUnprocessed, transactionMappings, charges);
|
||||
} else {
|
||||
transactionAmountUnprocessed = handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment,
|
||||
loanTransaction, transactionAmountUnprocessed, transactionMappings, charges);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
installmentIndex++;
|
||||
}
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
protected Set<LoanCharge> extractFeeCharges(final Set<LoanCharge> loanCharges) {
|
||||
final Set<LoanCharge> feeCharges = new HashSet<>();
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isFeeCharge()) {
|
||||
feeCharges.add(loanCharge);
|
||||
}
|
||||
}
|
||||
return feeCharges;
|
||||
}
|
||||
|
||||
protected Set<LoanCharge> extractPenaltyCharges(final Set<LoanCharge> loanCharges) {
|
||||
final Set<LoanCharge> penaltyCharges = new HashSet<>();
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
penaltyCharges.add(loanCharge);
|
||||
}
|
||||
}
|
||||
return penaltyCharges;
|
||||
}
|
||||
|
||||
protected void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges,
|
||||
final Integer installmentNumber) {
|
||||
|
||||
Money amountRemaining = feeCharges;
|
||||
while (amountRemaining.isGreaterThanZero()) {
|
||||
final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges, feeCharges.getCurrency());
|
||||
Money feeAmount = feeCharges.zero();
|
||||
if (loanTransaction.isChargePayment()) {
|
||||
feeAmount = feeCharges;
|
||||
}
|
||||
if (unpaidCharge == null) {
|
||||
break; // All are trache charges
|
||||
}
|
||||
final Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining, installmentNumber, feeAmount);
|
||||
if (!amountPaidTowardsCharge.isZero()) {
|
||||
Set<LoanChargePaidBy> chargesPaidBies = loanTransaction.getLoanChargesPaid();
|
||||
if (loanTransaction.isChargePayment()) {
|
||||
for (final LoanChargePaidBy chargePaidBy : chargesPaidBies) {
|
||||
LoanCharge loanCharge = chargePaidBy.getLoanCharge();
|
||||
if (loanCharge.getId().equals(unpaidCharge.getId())) {
|
||||
chargePaidBy.setAmount(amountPaidTowardsCharge.getAmount());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, unpaidCharge,
|
||||
amountPaidTowardsCharge.getAmount(), installmentNumber);
|
||||
chargesPaidBies.add(loanChargePaidBy);
|
||||
}
|
||||
amountRemaining = amountRemaining.minus(amountPaidTowardsCharge);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set<LoanCharge> charges, final MonetaryCurrency currency) {
|
||||
LoanCharge earliestUnpaidCharge = null;
|
||||
LoanCharge installemntCharge = null;
|
||||
LoanInstallmentCharge chargePerInstallment = null;
|
||||
for (final LoanCharge loanCharge : charges) {
|
||||
if (loanCharge.getAmountOutstanding(currency).isGreaterThanZero() && !loanCharge.isDueAtDisbursement()) {
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
LoanInstallmentCharge unpaidLoanChargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge();
|
||||
if (chargePerInstallment == null || DateUtils.isAfter(chargePerInstallment.getRepaymentInstallment().getDueDate(),
|
||||
unpaidLoanChargePerInstallment.getRepaymentInstallment().getDueDate())) {
|
||||
installemntCharge = loanCharge;
|
||||
chargePerInstallment = unpaidLoanChargePerInstallment;
|
||||
}
|
||||
} else if (earliestUnpaidCharge == null
|
||||
|| DateUtils.isBefore(loanCharge.getDueLocalDate(), earliestUnpaidCharge.getDueLocalDate())) {
|
||||
earliestUnpaidCharge = loanCharge;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (earliestUnpaidCharge == null || (chargePerInstallment != null && DateUtils.isAfter(earliestUnpaidCharge.getDueLocalDate(),
|
||||
chargePerInstallment.getRepaymentInstallment().getDueDate()))) {
|
||||
earliestUnpaidCharge = installemntCharge;
|
||||
}
|
||||
|
||||
return earliestUnpaidCharge;
|
||||
}
|
||||
|
||||
protected void handleWriteOff(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
|
||||
final List<LoanRepaymentScheduleInstallment> installments) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltychargesPortion = Money.zero(currency);
|
||||
|
||||
// determine how much is written off in total and breakdown for
|
||||
// principal, interest and charges
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
|
||||
if (currentInstallment.isNotFullyPaidOff()) {
|
||||
principalPortion = principalPortion.plus(currentInstallment.writeOffOutstandingPrincipal(transactionDate, currency));
|
||||
interestPortion = interestPortion.plus(currentInstallment.writeOffOutstandingInterest(transactionDate, currency));
|
||||
feeChargesPortion = feeChargesPortion.plus(currentInstallment.writeOffOutstandingFeeCharges(transactionDate, currency));
|
||||
penaltychargesPortion = penaltychargesPortion
|
||||
.plus(currentInstallment.writeOffOutstandingPenaltyCharges(transactionDate, currency));
|
||||
}
|
||||
}
|
||||
|
||||
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
|
||||
}
|
||||
|
||||
protected void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount,
|
||||
List<LoanRepaymentScheduleInstallment> installments) {
|
||||
processCreditTransaction(loanTransaction, overpaidAmount, currency, installments);
|
||||
}
|
||||
|
||||
protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) {
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
|
||||
installments.sort(Collections.reverseOrder(byDate));
|
||||
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
Money outstanding = currentInstallment.getTotalOutstanding(currency);
|
||||
Money due = currentInstallment.getDue(currency);
|
||||
|
||||
if (outstanding.isLessThan(due)) {
|
||||
transactionAmountUnprocessed = handleRefundTransactionPaymentOfInstallment(currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, transactionMappings);
|
||||
|
||||
}
|
||||
|
||||
if (transactionAmountUnprocessed.isZero()) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
|
||||
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
|
||||
Integer installmentNumber = null;
|
||||
|
||||
final Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
|
||||
if (feeCharges.isGreaterThanZero()) {
|
||||
undoChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, installmentNumber);
|
||||
}
|
||||
|
||||
final Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
|
||||
if (penaltyCharges.isGreaterThanZero()) {
|
||||
undoChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, installmentNumber);
|
||||
}
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
}
|
||||
|
||||
protected void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges,
|
||||
final Integer installmentNumber) {
|
||||
|
||||
Money amountRemaining = feeCharges;
|
||||
while (amountRemaining.isGreaterThanZero()) {
|
||||
final LoanCharge paidCharge = findLatestPaidChargeFromUnOrderedSet(charges, feeCharges.getCurrency());
|
||||
|
||||
if (paidCharge != null) {
|
||||
Money feeAmount = feeCharges.zero();
|
||||
|
||||
final Money amountDeductedTowardsCharge = paidCharge.undoPaidOrPartiallyAmountBy(amountRemaining, installmentNumber,
|
||||
feeAmount);
|
||||
if (amountDeductedTowardsCharge.isGreaterThanZero()) {
|
||||
|
||||
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, paidCharge,
|
||||
amountDeductedTowardsCharge.getAmount().multiply(new BigDecimal(-1)), null);
|
||||
loanTransaction.getLoanChargesPaid().add(loanChargePaidBy);
|
||||
|
||||
amountRemaining = amountRemaining.minus(amountDeductedTowardsCharge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private LoanCharge findLatestPaidChargeFromUnOrderedSet(final Set<LoanCharge> charges, MonetaryCurrency currency) {
|
||||
LoanCharge latestPaidCharge = null;
|
||||
LoanCharge installemntCharge = null;
|
||||
LoanInstallmentCharge chargePerInstallment = null;
|
||||
for (final LoanCharge loanCharge : charges) {
|
||||
boolean isPaidOrPartiallyPaid = loanCharge.isPaidOrPartiallyPaid(currency);
|
||||
if (isPaidOrPartiallyPaid && !loanCharge.isDueAtDisbursement()) {
|
||||
if (loanCharge.isInstalmentFee()) {
|
||||
LoanInstallmentCharge paidLoanChargePerInstallment = loanCharge
|
||||
.getLastPaidOrPartiallyPaidInstallmentLoanCharge(currency);
|
||||
if (chargePerInstallment == null || (paidLoanChargePerInstallment != null
|
||||
&& DateUtils.isBefore(chargePerInstallment.getRepaymentInstallment().getDueDate(),
|
||||
paidLoanChargePerInstallment.getRepaymentInstallment().getDueDate()))) {
|
||||
installemntCharge = loanCharge;
|
||||
chargePerInstallment = paidLoanChargePerInstallment;
|
||||
}
|
||||
} else if (latestPaidCharge == null || (loanCharge.isPaidOrPartiallyPaid(currency)
|
||||
&& DateUtils.isAfter(loanCharge.getDueLocalDate(), latestPaidCharge.getDueLocalDate()))) {
|
||||
latestPaidCharge = loanCharge;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (latestPaidCharge == null || (chargePerInstallment != null
|
||||
&& DateUtils.isAfter(latestPaidCharge.getDueLocalDate(), chargePerInstallment.getRepaymentInstallment().getDueDate()))) {
|
||||
latestPaidCharge = installemntCharge;
|
||||
}
|
||||
|
||||
return latestPaidCharge;
|
||||
}
|
||||
}
|
||||
+839
@@ -0,0 +1,839 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import static java.util.stream.Collectors.mapping;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.DueType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Slf4j
|
||||
public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
|
||||
|
||||
public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Advanced payment allocation strategy";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
|
||||
List<LoanRepaymentScheduleInstallment> installments, LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
|
||||
List<LoanRepaymentScheduleInstallment> installments, LoanTransaction loanTransaction, Money paymentInAdvance,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
|
||||
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleRefundTransactionPaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
|
||||
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money handleRepaymentSchedule(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> loanCharges) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
|
||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
if (charges != null) {
|
||||
for (final LoanCharge loanCharge : charges) {
|
||||
if (!loanCharge.isDueAtDisbursement()) {
|
||||
loanCharge.resetPaidAmount(currency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
currentInstallment.resetBalances();
|
||||
currentInstallment.updateDerivedFields(currency, disbursementDate);
|
||||
}
|
||||
|
||||
List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges);
|
||||
|
||||
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
|
||||
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
|
||||
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency,
|
||||
installments, charges, changedTransactionDetail));
|
||||
chargeOrTransaction.getLoanCharge()
|
||||
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
|
||||
}
|
||||
reprocessInstallments(installments, currency);
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, Money overpaidAmount) {
|
||||
switch (loanTransaction.getTypeOf()) {
|
||||
case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments);
|
||||
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
|
||||
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
|
||||
case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments);
|
||||
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
|
||||
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
|
||||
handleRepayment(loanTransaction, currency, installments, charges);
|
||||
case CHARGE_OFF -> handleChargeOff(loanTransaction, currency, installments);
|
||||
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, currency, installments, charges);
|
||||
// TODO: Cover rest of the transaction types
|
||||
default -> {
|
||||
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
Money zero = Money.zero(currency);
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
||||
|
||||
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
|
||||
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
|
||||
LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
|
||||
.orElse(defaultPaymentAllocationRule);
|
||||
Balances balances = new Balances(zero, zero, zero, zero);
|
||||
List<PaymentAllocationType> paymentAllocationTypes;
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule;
|
||||
if (PaymentAllocationTransactionType.DEFAULT.equals(paymentAllocationRule.getTransactionType())) {
|
||||
// if the allocation rule is not defined then the reverse order of the default allocation rule will be used
|
||||
paymentAllocationTypes = new ArrayList<>(paymentAllocationRule.getAllocationTypes());
|
||||
Collections.reverse(paymentAllocationTypes);
|
||||
futureInstallmentAllocationRule = FutureInstallmentAllocationRule.LAST_INSTALLMENT;
|
||||
} else {
|
||||
paymentAllocationTypes = paymentAllocationRule.getAllocationTypes();
|
||||
futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
|
||||
}
|
||||
if (LoanScheduleProcessingType.HORIZONTAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
// TODO: implementation
|
||||
} else if (LoanScheduleProcessingType.VERTICAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
|
||||
transactionAmountUnprocessed = refundTransaction(loanTransaction, currency, installments, zero, transactionMappings,
|
||||
transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, paymentAllocationType);
|
||||
if (!transactionAmountUnprocessed.isGreaterThanZero()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
|
||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
|
||||
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
|
||||
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
|
||||
Integer installmentNumber = null;
|
||||
|
||||
final Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
|
||||
if (feeCharges.isGreaterThanZero()) {
|
||||
undoChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, installmentNumber);
|
||||
}
|
||||
|
||||
final Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
|
||||
if (penaltyCharges.isGreaterThanZero()) {
|
||||
undoChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, installmentNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges,
|
||||
ChangedTransactionDetail changedTransactionDetail) {
|
||||
if (loanTransaction.getId() == null) {
|
||||
processLatestTransaction(loanTransaction, currency, installments, charges, Money.zero(currency));
|
||||
loanTransaction.adjustInterestComponent(currency);
|
||||
} else {
|
||||
/*
|
||||
* For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has
|
||||
* changed.<br>
|
||||
*/
|
||||
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
|
||||
|
||||
// Reset derived component of new loan transaction and
|
||||
// re-process transaction
|
||||
processLatestTransaction(newLoanTransaction, currency, installments, charges, Money.zero(currency));
|
||||
newLoanTransaction.adjustInterestComponent(currency);
|
||||
/*
|
||||
* Check if the transaction amounts have changed. If so, reverse the original transaction and update
|
||||
* changedTransactionDetail accordingly
|
||||
*/
|
||||
if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
|
||||
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
|
||||
} else {
|
||||
createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processSingleCharge(LoanCharge loanCharge, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
||||
LocalDate disbursementDate) {
|
||||
loanChargeProcessor.reprocess(currency, disbursementDate, installments, loanCharge);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<ChargeOrTransaction> createSortedChargesAndTransactionsList(List<LoanTransaction> loanTransactions,
|
||||
Set<LoanCharge> charges) {
|
||||
List<ChargeOrTransaction> chargeOrTransactions = new ArrayList<>();
|
||||
if (charges != null) {
|
||||
chargeOrTransactions.addAll(charges.stream().map(ChargeOrTransaction::new).toList());
|
||||
}
|
||||
if (loanTransactions != null) {
|
||||
chargeOrTransactions.addAll(loanTransactions.stream().map(ChargeOrTransaction::new).toList());
|
||||
}
|
||||
Collections.sort(chargeOrTransactions);
|
||||
return chargeOrTransactions;
|
||||
}
|
||||
|
||||
private void handleDisbursement(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments) {
|
||||
updateLoanSchedule(loanTransaction, currency, installments);
|
||||
}
|
||||
|
||||
private void updateLoanSchedule(LoanTransaction disbursementTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments) {
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
|
||||
i -> !i.getDueDate().isBefore(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
||||
.toList();
|
||||
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
|
||||
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
Money downPaymentAmount = Money.zero(currency);
|
||||
if (loanProductRelatedDetail.isEnableDownPayment()) {
|
||||
LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream()
|
||||
.filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow();
|
||||
BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(),
|
||||
loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc);
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf);
|
||||
}
|
||||
downPaymentAmount = Money.of(currency, downPaymentAmt);
|
||||
downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount);
|
||||
|
||||
}
|
||||
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
|
||||
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, mc.getRoundingMode());
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
increasePrincipalBy = Money.roundToMultiplesOf(increasePrincipalBy, installmentAmountInMultiplesOf);
|
||||
}
|
||||
Money remainingAmount = increasePrincipalBy.multiplyRetainScale(noCandidateRepaymentInstallments, mc.getRoundingMode())
|
||||
.minus(amortizableAmount);
|
||||
|
||||
Money finalIncreasePrincipalBy = increasePrincipalBy;
|
||||
candidateRepaymentInstallments
|
||||
.forEach(i -> i.addToPrincipal(disbursementTransaction.getTransactionDate(), finalIncreasePrincipalBy));
|
||||
// Hence the rounding, we might need to amend the last installment amount
|
||||
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1)
|
||||
.addToPrincipal(disbursementTransaction.getTransactionDate(), remainingAmount);
|
||||
}
|
||||
|
||||
private void handleRepayment(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
|
||||
loanTransaction.resetDerivedComponents();
|
||||
}
|
||||
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
||||
processTransaction(loanTransaction, currency, installments, transactionAmountUnprocessed, charges);
|
||||
}
|
||||
|
||||
private LoanTransactionToRepaymentScheduleMapping getTransactionMapping(
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, LoanTransaction loanTransaction,
|
||||
LoanRepaymentScheduleInstallment currentInstallment, MonetaryCurrency currency) {
|
||||
Money zero = Money.zero(currency);
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = transactionMappings.stream()
|
||||
.filter(e -> loanTransaction.equals(e.getLoanTransaction()))
|
||||
.filter(e -> currentInstallment.equals(e.getLoanRepaymentScheduleInstallment())).findFirst().orElse(null);
|
||||
if (loanTransactionToRepaymentScheduleMapping == null) {
|
||||
loanTransactionToRepaymentScheduleMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
|
||||
currentInstallment, zero, zero, zero, zero);
|
||||
transactionMappings.add(loanTransactionToRepaymentScheduleMapping);
|
||||
}
|
||||
return loanTransactionToRepaymentScheduleMapping;
|
||||
}
|
||||
|
||||
private Money processPaymentAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment,
|
||||
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction action) {
|
||||
LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
Money zero = transactionAmountUnprocessed.zero();
|
||||
return switch (paymentAllocationType.getAllocationType()) {
|
||||
case PENALTY -> {
|
||||
LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment
|
||||
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
|
||||
Money portion = paymentFunction.accept(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(portion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, portion);
|
||||
yield portion;
|
||||
}
|
||||
case FEE -> {
|
||||
LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
|
||||
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
|
||||
Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(portion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, portion, zero);
|
||||
yield portion;
|
||||
}
|
||||
case INTEREST -> {
|
||||
LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
|
||||
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
|
||||
Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(portion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, portion, zero, zero);
|
||||
yield portion;
|
||||
}
|
||||
case PRINCIPAL -> {
|
||||
LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
|
||||
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
|
||||
Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(portion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, portion, zero, zero, zero);
|
||||
yield portion;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping,
|
||||
Money principalPortion, Money interestPortion, Money feePortion, Money penaltyPortion) {
|
||||
BigDecimal aggregatedPenalty = ObjectUtils
|
||||
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getPenaltyChargesPortion(), BigDecimal.ZERO)
|
||||
.add(penaltyPortion.getAmount());
|
||||
BigDecimal aggregatedFee = ObjectUtils
|
||||
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getFeeChargesPortion(), BigDecimal.ZERO)
|
||||
.add(feePortion.getAmount());
|
||||
BigDecimal aggregatedInterest = ObjectUtils
|
||||
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getInterestPortion(), BigDecimal.ZERO)
|
||||
.add(interestPortion.getAmount());
|
||||
BigDecimal aggregatedPrincipal = ObjectUtils
|
||||
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getPrincipalPortion(), BigDecimal.ZERO)
|
||||
.add(principalPortion.getAmount());
|
||||
loanTransactionToRepaymentScheduleMapping.setComponents(aggregatedPrincipal, aggregatedInterest, aggregatedFee, aggregatedPenalty);
|
||||
}
|
||||
|
||||
private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction) {
|
||||
if (overpaymentPortion.isGreaterThanZero()) {
|
||||
onLoanOverpayment(loanTransaction, overpaymentPortion);
|
||||
loanTransaction.updateOverPayments(overpaymentPortion);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChargeOff(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments) {
|
||||
loanTransaction.resetDerivedComponents();
|
||||
// determine how much is outstanding total and breakdown for principal, interest and charges
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltychargesPortion = Money.zero(currency);
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
if (currentInstallment.isNotFullyPaidOff()) {
|
||||
principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(currency));
|
||||
interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(currency));
|
||||
feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesOutstanding(currency));
|
||||
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesCharged(currency));
|
||||
}
|
||||
}
|
||||
|
||||
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
|
||||
}
|
||||
|
||||
private void handleChargePayment(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
Money zero = Money.zero(currency);
|
||||
Money feeChargesPortion = zero;
|
||||
Money penaltyChargesPortion = zero;
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
LoanChargePaidBy loanChargePaidBy = loanTransaction.getLoanChargesPaid().stream().findFirst().get();
|
||||
LoanCharge loanCharge = loanChargePaidBy.getLoanCharge();
|
||||
Money amountToBePaid = Money.of(currency, loanTransaction.getAmount());
|
||||
if (loanCharge.getAmountOutstanding(currency).isLessThan(amountToBePaid)) {
|
||||
amountToBePaid = loanCharge.getAmountOutstanding(currency);
|
||||
}
|
||||
|
||||
LocalDate startDate = loanTransaction.getLoan().getDisbursementDate();
|
||||
|
||||
Money unprocessed = loanTransaction.getAmount(currency);
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
boolean isDue = installment.isFirstPeriod()
|
||||
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(startDate, installment.getDueDate())
|
||||
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate());
|
||||
if (isDue) {
|
||||
Integer installmentNumber = installment.getInstallmentNumber();
|
||||
Money paidAmount = loanCharge.updatePaidAmountBy(amountToBePaid, installmentNumber, zero);
|
||||
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, installment, currency);
|
||||
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = installment.payPenaltyChargesComponent(loanTransaction.getTransactionDate(), paidAmount);
|
||||
loanTransaction.setLoanChargesPaid(Collections
|
||||
.singleton(new LoanChargePaidBy(loanTransaction, loanCharge, paidAmount.getAmount(), installmentNumber)));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = installment.payFeeChargesComponent(loanTransaction.getTransactionDate(), paidAmount);
|
||||
loanTransaction.setLoanChargesPaid(Collections
|
||||
.singleton(new LoanChargePaidBy(loanTransaction, loanCharge, paidAmount.getAmount(), installmentNumber)));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, feeChargesPortion, zero);
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(zero, zero, feeChargesPortion, penaltyChargesPortion);
|
||||
unprocessed = loanTransaction.getAmount(currency).minus(paidAmount);
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
}
|
||||
}
|
||||
|
||||
if (unprocessed.isGreaterThanZero()) {
|
||||
processTransaction(loanTransaction, currency, installments, unprocessed, charges);
|
||||
}
|
||||
}
|
||||
|
||||
private Money refundTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money zero,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Money transactionAmountUnprocessed,
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule, Balances balances,
|
||||
PaymentAllocationType paymentAllocationType) {
|
||||
LoanRepaymentScheduleInstallment currentInstallment = null;
|
||||
Money refundedPortion = zero;
|
||||
do {
|
||||
switch (paymentAllocationType.getDueType()) {
|
||||
case PAST_DUE -> {
|
||||
currentInstallment = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
||||
.filter(e -> loanTransaction.isAfter(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||
if (currentInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
||||
}
|
||||
}
|
||||
case DUE -> {
|
||||
currentInstallment = installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
||||
.filter(installment -> loanTransaction.isOn(installment.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||
if (currentInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
||||
}
|
||||
}
|
||||
case IN_ADVANCE -> {
|
||||
List<LoanRepaymentScheduleInstallment> currentInstallments = new ArrayList<>();
|
||||
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
|
||||
currentInstallments = installments.stream()
|
||||
.filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())).toList();
|
||||
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
currentInstallments = installments.stream()
|
||||
.filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
currentInstallments = installments.stream()
|
||||
.filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
}
|
||||
int numberOfInstallments = currentInstallments.size();
|
||||
refundedPortion = zero;
|
||||
if (numberOfInstallments > 0) {
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
|
||||
currentInstallment = internalCurrentInstallment;
|
||||
if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) {
|
||||
evenPortion = evenPortion.add(balanceAdjustment);
|
||||
}
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
Money internalUnpaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment,
|
||||
loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
||||
if (internalUnpaidPortion.isGreaterThanZero()) {
|
||||
refundedPortion = internalUnpaidPortion;
|
||||
}
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(internalUnpaidPortion);
|
||||
}
|
||||
} else {
|
||||
currentInstallment = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (currentInstallment != null && transactionAmountUnprocessed.isGreaterThanZero() && refundedPortion.isGreaterThanZero());
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
private void processTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed, Set<LoanCharge> charges) {
|
||||
Money zero = Money.zero(currency);
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
|
||||
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
|
||||
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
|
||||
LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
|
||||
.orElse(defaultPaymentAllocationRule);
|
||||
Balances balances = new Balances(zero, zero, zero, zero);
|
||||
|
||||
if (LoanScheduleProcessingType.HORIZONTAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, currency, installments, transactionAmountUnprocessed,
|
||||
paymentAllocationRule, transactionMappings, balances);
|
||||
} else if (LoanScheduleProcessingType.VERTICAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, currency, installments, transactionAmountUnprocessed,
|
||||
paymentAllocationRule, transactionMappings, balances);
|
||||
}
|
||||
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
|
||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
|
||||
payAdditionalCharges(loanTransaction, currency, charges);
|
||||
|
||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction);
|
||||
}
|
||||
|
||||
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
|
||||
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Balances balances) {
|
||||
LinkedHashMap<DueType, List<PaymentAllocationType>> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream()
|
||||
.collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new,
|
||||
mapping(Function.identity(), toList())));
|
||||
|
||||
for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
|
||||
transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, currency, installments,
|
||||
transactionAmountUnprocessed, paymentAllocationsEntry.getValue(),
|
||||
paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, balances);
|
||||
}
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
private Money processAllocationsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
|
||||
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances) {
|
||||
Money paidPortion;
|
||||
boolean exit = false;
|
||||
do {
|
||||
LoanRepaymentScheduleInstallment oldestPastDueInstallment = installments.stream()
|
||||
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isAfter(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||
LoanRepaymentScheduleInstallment dueInstallment = installments.stream()
|
||||
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isOn(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||
|
||||
// For having similar logic we are populating installment list even when the future installment
|
||||
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
|
||||
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new ArrayList<>();
|
||||
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
|
||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())).toList();
|
||||
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
}
|
||||
|
||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
|
||||
switch (paymentAllocationType.getDueType()) {
|
||||
case PAST_DUE -> {
|
||||
if (oldestPastDueInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, oldestPastDueInstallment, currency);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
} else {
|
||||
exit = true;
|
||||
}
|
||||
}
|
||||
case DUE -> {
|
||||
if (dueInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, dueInstallment, currency);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
} else {
|
||||
exit = true;
|
||||
}
|
||||
}
|
||||
case IN_ADVANCE -> {
|
||||
int numberOfInstallments = inAdvanceInstallments.size();
|
||||
if (numberOfInstallments > 0) {
|
||||
// This will be the same amount as transactionAmountUnprocessed in case of the future
|
||||
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
|
||||
// Adjustment might be needed due to the divide operation and the rounding mode
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
|
||||
// Adjust the portion for the last installment
|
||||
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
|
||||
evenPortion = evenPortion.add(balanceAdjustment);
|
||||
}
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, inAdvanceInstallment, currency);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
|
||||
evenPortion, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
}
|
||||
} else {
|
||||
exit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We are allocating till there is no pending installment or there is no more unprocessed transaction amount
|
||||
// or there is no more outstanding balance of the allocation type
|
||||
while (!exit && installments.stream().anyMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
&& transactionAmountUnprocessed.isGreaterThanZero());
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
private Money processPeriodsVertically(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
|
||||
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Balances balances) {
|
||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) {
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
|
||||
LoanRepaymentScheduleInstallment currentInstallment = null;
|
||||
Money paidPortion = Money.zero(currency);
|
||||
do {
|
||||
Predicate<LoanRepaymentScheduleInstallment> predicate = getFilterPredicate(paymentAllocationType, currency);
|
||||
switch (paymentAllocationType.getDueType()) {
|
||||
case PAST_DUE -> {
|
||||
currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isAfter(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||
if (currentInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
}
|
||||
}
|
||||
case DUE -> {
|
||||
currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isOn(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||
if (currentInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
}
|
||||
}
|
||||
case IN_ADVANCE -> {
|
||||
// For having similar logic we are populating installment list even when the future installment
|
||||
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
|
||||
List<LoanRepaymentScheduleInstallment> currentInstallments = new ArrayList<>();
|
||||
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
|
||||
currentInstallments = installments.stream().filter(predicate)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())).toList();
|
||||
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
currentInstallments = installments.stream().filter(predicate)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
currentInstallments = installments.stream().filter(predicate)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
}
|
||||
int numberOfInstallments = currentInstallments.size();
|
||||
paidPortion = Money.zero(currency);
|
||||
if (numberOfInstallments > 0) {
|
||||
// This will be the same amount as transactionAmountUnprocessed in case of the future
|
||||
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
|
||||
// Adjustment might be needed due to the divide operation and the rounding mode
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
|
||||
currentInstallment = internalCurrentInstallment;
|
||||
// Adjust the portion for the last installment
|
||||
if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) {
|
||||
evenPortion = evenPortion.add(balanceAdjustment);
|
||||
}
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
Money internalPaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment,
|
||||
loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
// Some extra logic to allocate as much as possible across the installments if the
|
||||
// outstanding balances are different
|
||||
if (internalPaidPortion.isGreaterThanZero()) {
|
||||
paidPortion = internalPaidPortion;
|
||||
}
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(internalPaidPortion);
|
||||
}
|
||||
} else {
|
||||
currentInstallment = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We are allocating till there is no pending installment or there is no more unprocessed transaction amount
|
||||
// or there is no more outstanding balance of the allocation type
|
||||
while (currentInstallment != null && transactionAmountUnprocessed.isGreaterThanZero() && paidPortion.isGreaterThanZero());
|
||||
}
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
private Predicate<LoanRepaymentScheduleInstallment> getFilterPredicate(PaymentAllocationType paymentAllocationType,
|
||||
MonetaryCurrency currency) {
|
||||
return switch (paymentAllocationType.getAllocationType()) {
|
||||
case PENALTY -> (p) -> p.getPenaltyChargesOutstanding(currency).isGreaterThanZero();
|
||||
case FEE -> (p) -> p.getFeeChargesOutstanding(currency).isGreaterThanZero();
|
||||
case INTEREST -> (p) -> p.getInterestOutstanding(currency).isGreaterThanZero();
|
||||
case PRINCIPAL -> (p) -> p.getPrincipalOutstanding(currency).isGreaterThanZero();
|
||||
};
|
||||
}
|
||||
|
||||
private void payAdditionalCharges(LoanTransaction loanTransaction, MonetaryCurrency currency, Set<LoanCharge> charges) {
|
||||
final Set<LoanCharge> paidFeeCharges = loanTransaction.getLoanChargesPaid().stream() //
|
||||
.map(LoanChargePaidBy::getLoanCharge) //
|
||||
.filter(LoanCharge::isFeeCharge).collect(Collectors.toSet());
|
||||
final Set<LoanCharge> paidPenaltyCharges = loanTransaction.getLoanChargesPaid().stream() //
|
||||
.map(LoanChargePaidBy::getLoanCharge) //
|
||||
.filter(LoanCharge::isPenaltyCharge).collect(Collectors.toSet());
|
||||
// TODO: rewrite to provide sorted list
|
||||
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
|
||||
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
|
||||
loanFees.removeAll(paidFeeCharges);
|
||||
loanPenalties.removeAll(paidPenaltyCharges);
|
||||
|
||||
BigDecimal sumFeePaidAmount = paidFeeCharges.stream() //
|
||||
.map(paidCharge -> paidCharge.getAmountPaid(currency)) //
|
||||
.map(Money::getAmount) //
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal sumPenaltyPaidAmount = paidPenaltyCharges.stream() //
|
||||
.map(paidCharge -> paidCharge.getAmountPaid(currency)) //
|
||||
.map(Money::getAmount) //
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
if (loanTransaction.isNotWaiver() && !loanTransaction.isAccrual()) {
|
||||
Money feeCharges = loanTransaction.getFeeChargesPortion(currency).minus(sumFeePaidAmount);
|
||||
Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency).minus(sumPenaltyPaidAmount);
|
||||
|
||||
if (feeCharges.isGreaterThanZero()) {
|
||||
updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, null);
|
||||
}
|
||||
|
||||
if (penaltyCharges.isGreaterThanZero()) {
|
||||
updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
private static class Balances {
|
||||
|
||||
private Money aggregatedPrincipalPortion;
|
||||
private Money aggregatedFeeChargesPortion;
|
||||
private Money aggregatedInterestPortion;
|
||||
private Money aggregatedPenaltyChargesPortion;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Optional;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
|
||||
|
||||
private final Optional<LoanCharge> loanCharge;
|
||||
private final Optional<LoanTransaction> loanTransaction;
|
||||
|
||||
public ChargeOrTransaction(LoanCharge loanCharge) {
|
||||
this.loanCharge = Optional.of(loanCharge);
|
||||
this.loanTransaction = Optional.empty();
|
||||
}
|
||||
|
||||
public ChargeOrTransaction(LoanTransaction loanTransaction) {
|
||||
this.loanTransaction = Optional.of(loanTransaction);
|
||||
this.loanCharge = Optional.empty();
|
||||
}
|
||||
|
||||
private LocalDate getEffectiveDate() {
|
||||
if (loanCharge.isPresent()) {
|
||||
return loanCharge.get().getDueDate();
|
||||
} else if (loanTransaction.isPresent()) {
|
||||
return loanTransaction.get().getTransactionDate();
|
||||
} else {
|
||||
throw new RuntimeException("Either charge or transaction should be present");
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDate getSubmittedOnDate() {
|
||||
if (loanCharge.isPresent()) {
|
||||
return loanCharge.get().getSubmittedOnDate();
|
||||
} else if (loanTransaction.isPresent()) {
|
||||
return loanTransaction.get().getSubmittedOnDate();
|
||||
} else {
|
||||
throw new RuntimeException("Either charge or transaction should be present");
|
||||
}
|
||||
}
|
||||
|
||||
private OffsetDateTime getCreatedDateTime() {
|
||||
if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) {
|
||||
return loanCharge.get().getCreatedDate().get();
|
||||
} else if (loanTransaction.isPresent()) {
|
||||
return loanTransaction.get().getCreatedDateTime();
|
||||
} else {
|
||||
throw new RuntimeException("Either charge with createdDate or transaction created datetime should be present");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull ChargeOrTransaction o) {
|
||||
int datePortion = this.getEffectiveDate().compareTo(o.getEffectiveDate());
|
||||
if (datePortion == 0) {
|
||||
int submittedDate = this.getSubmittedOnDate().compareTo(o.getSubmittedOnDate());
|
||||
if (submittedDate == 0) {
|
||||
return this.getCreatedDateTime().compareTo(o.getCreatedDateTime());
|
||||
}
|
||||
return submittedDate;
|
||||
}
|
||||
return datePortion;
|
||||
}
|
||||
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
/**
|
||||
* Creocore style {@link LoanRepaymentScheduleTransactionProcessor}.
|
||||
*
|
||||
* For standard transactions, pays off components in order of interest, then principal.
|
||||
*
|
||||
* If a transaction results in an advance payment or over-payment for a given installment, the over paid amount is pay
|
||||
* off on the principal component of subsequent installments.
|
||||
*
|
||||
* If the entire principal of an installment is paid in advance then the interest component is waived.
|
||||
*/
|
||||
public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public static final String STRATEGY_CODE = "creocore-strategy";
|
||||
|
||||
public static final String STRATEGY_NAME = "Creocore Unique";
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return STRATEGY_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return STRATEGY_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* For early/'in advance' repayments, pay off in the same way as on-time payments, interest first then principal.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Override
|
||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
|
||||
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
|
||||
charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For late repayments, pay off in the same way as on-time payments, interest first then principal.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Override
|
||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
|
||||
final Money transactionAmountUnprocessed, final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
|
||||
transactionMappings, charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For normal on-time repayments, pays off interest first, then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
} else {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
}
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeEffectiveDueDateComparator;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
/**
|
||||
* `First due/late charges, interest, principal, after in advance principal, charges, interest` style
|
||||
* {@link LoanRepaymentScheduleTransactionProcessor}.
|
||||
*
|
||||
* For ALL types of transactions, pays off components in order of: Due/late penalty Due/late Fee Due/late interest
|
||||
* Due/late principal In advance principal In advance penalty In advance fee In advance interest
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor
|
||||
extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public static final String STRATEGY_CODE = "due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest-strategy";
|
||||
|
||||
public static final String STRATEGY_NAME = "Due penalty, fee, interest, principal, In advance principal, penalty, fee, interest";
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return STRATEGY_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return STRATEGY_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* For early/'in advance' repayments
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
|
||||
final MonetaryCurrency currency = paymentInAdvance.getCurrency();
|
||||
Money transactionAmountRemaining = paymentInAdvance;
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltyChargesPortion = Money.zero(currency);
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else {
|
||||
boolean ignoreDueDateCheck = false;
|
||||
boolean rerun = false;
|
||||
|
||||
List<LoanCharge> orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isChargePending)
|
||||
.filter(loanCharge -> loanCharge.getEffectiveDueDate() == null
|
||||
|| !DateUtils.isAfter(loanCharge.getEffectiveDueDate(), transactionDate))
|
||||
.sorted(LoanChargeEffectiveDueDateComparator.INSTANCE).toList();
|
||||
Money calculatedPenaltyCharge = Money.zero(currency);
|
||||
Money calculatedFeeCharge = Money.zero(currency);
|
||||
// Calculate the amount of due charges
|
||||
for (LoanCharge charge : orderedLoanChargesByDueDate) {
|
||||
if (charge.isPenaltyCharge()) {
|
||||
calculatedPenaltyCharge = calculatedPenaltyCharge.add(charge.getAmount(currency));
|
||||
} else {
|
||||
calculatedFeeCharge = calculatedFeeCharge.add(charge.getAmount(currency));
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
Money subPenaltyPortion;
|
||||
if (!ignoreDueDateCheck) {
|
||||
if (calculatedPenaltyCharge.isGreaterThan(transactionAmountRemaining)) {
|
||||
calculatedPenaltyCharge = transactionAmountRemaining;
|
||||
}
|
||||
} else {
|
||||
calculatedPenaltyCharge = transactionAmountRemaining;
|
||||
}
|
||||
subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, calculatedPenaltyCharge);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subPenaltyPortion);
|
||||
penaltyChargesPortion = penaltyChargesPortion.add(subPenaltyPortion);
|
||||
|
||||
Money subFeePortion;
|
||||
if (!ignoreDueDateCheck) {
|
||||
if (calculatedFeeCharge.isGreaterThan(transactionAmountRemaining)) {
|
||||
calculatedFeeCharge = transactionAmountRemaining;
|
||||
}
|
||||
} else {
|
||||
calculatedFeeCharge = transactionAmountRemaining;
|
||||
}
|
||||
subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, calculatedFeeCharge);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion);
|
||||
feeChargesPortion = feeChargesPortion.add(subFeePortion);
|
||||
|
||||
Money subInterestPortion;
|
||||
if (ignoreDueDateCheck || !DateUtils.isBefore(transactionDate, currentInstallment.getDueDate())) {
|
||||
subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subInterestPortion);
|
||||
interestPortion = interestPortion.add(subInterestPortion);
|
||||
}
|
||||
|
||||
Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subPrincipalPortion);
|
||||
principalPortion = principalPortion.add(subPrincipalPortion);
|
||||
// If the transactionAmountRemaining is greater than zero, rerun the allocation without due date check
|
||||
// to distribute the in advance portions
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
ignoreDueDateCheck = true;
|
||||
}
|
||||
rerun = !rerun;
|
||||
} while (ignoreDueDateCheck && rerun);
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
}
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* For late repayments
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
|
||||
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
|
||||
transactionMappings, charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For normal on-time repayments
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltyChargesPortion = Money.zero(currency);
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else {
|
||||
Money subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subPenaltyPortion);
|
||||
penaltyChargesPortion = penaltyChargesPortion.add(subPenaltyPortion);
|
||||
|
||||
Money subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion);
|
||||
feeChargesPortion = feeChargesPortion.add(subFeePortion);
|
||||
|
||||
Money subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subInterestPortion);
|
||||
interestPortion = interestPortion.add(subInterestPortion);
|
||||
|
||||
Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subPrincipalPortion);
|
||||
principalPortion = principalPortion.add(subPrincipalPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
}
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltyChargesPortion = Money.zero(currency);
|
||||
|
||||
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeEffectiveDueDateComparator;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
/**
|
||||
* `First due/late penalty, interest, principal, fee, after in advance penalty, interest, principal, fee` style
|
||||
* {@link LoanRepaymentScheduleTransactionProcessor}.
|
||||
*
|
||||
* For ALL types of transactions, pays off components in order of: Due/late penalty, Due/late interest, Due/late
|
||||
* principal, Due/late fee, In advance penalty, In advance interest In advance principal, In advance fee
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor
|
||||
extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public static final String STRATEGY_CODE = "due-penalty-interest-principal-fee-in-advance-penalty-interest-principal-fee-strategy";
|
||||
|
||||
public static final String STRATEGY_NAME = "Due penalty, interest, principal, fee, In advance penalty, interest, principal, fee";
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return STRATEGY_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return STRATEGY_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* For early/'in advance' repayments
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money inAdvancePayment,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
|
||||
final MonetaryCurrency currency = inAdvancePayment.getCurrency();
|
||||
Money transactionAmountRemaining = inAdvancePayment;
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltyChargesPortion = Money.zero(currency);
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else {
|
||||
// Due penalty, interest, principal, fee, In advance penalty, interest, principal, fee
|
||||
boolean ignoreDueDateCheck = false;
|
||||
boolean rerun = false;
|
||||
|
||||
List<LoanCharge> orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isChargePending)
|
||||
.filter(loanCharge -> loanCharge.getEffectiveDueDate() == null
|
||||
|| !DateUtils.isAfter(loanCharge.getEffectiveDueDate(), transactionDate))
|
||||
.sorted(LoanChargeEffectiveDueDateComparator.INSTANCE).toList();
|
||||
Money calculatedPenaltyCharge = Money.zero(currency);
|
||||
Money calculatedFeeCharge = Money.zero(currency);
|
||||
// Calculate the amount of due charges
|
||||
for (LoanCharge charge : orderedLoanChargesByDueDate) {
|
||||
if (charge.isPenaltyCharge()) {
|
||||
calculatedPenaltyCharge = calculatedPenaltyCharge.add(charge.getAmount(currency));
|
||||
} else {
|
||||
calculatedFeeCharge = calculatedFeeCharge.add(charge.getAmount(currency));
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
Money subPenaltyPortion;
|
||||
if (!ignoreDueDateCheck) {
|
||||
if (calculatedPenaltyCharge.isGreaterThan(transactionAmountRemaining)) {
|
||||
calculatedPenaltyCharge = transactionAmountRemaining;
|
||||
}
|
||||
} else {
|
||||
calculatedPenaltyCharge = transactionAmountRemaining;
|
||||
}
|
||||
subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, calculatedPenaltyCharge);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subPenaltyPortion);
|
||||
penaltyChargesPortion = penaltyChargesPortion.add(subPenaltyPortion);
|
||||
|
||||
Money subInterestPortion;
|
||||
if (ignoreDueDateCheck || !DateUtils.isBefore(transactionDate, currentInstallment.getDueDate())) {
|
||||
subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subInterestPortion);
|
||||
interestPortion = interestPortion.add(subInterestPortion);
|
||||
|
||||
Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subPrincipalPortion);
|
||||
principalPortion = principalPortion.add(subPrincipalPortion);
|
||||
}
|
||||
|
||||
Money subFeePortion;
|
||||
if (!ignoreDueDateCheck) {
|
||||
if (calculatedFeeCharge.isGreaterThan(transactionAmountRemaining)) {
|
||||
calculatedFeeCharge = transactionAmountRemaining;
|
||||
}
|
||||
} else {
|
||||
calculatedFeeCharge = transactionAmountRemaining;
|
||||
}
|
||||
subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, calculatedFeeCharge);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion);
|
||||
feeChargesPortion = feeChargesPortion.add(subFeePortion);
|
||||
|
||||
// If the transactionAmountRemaining is greater than zero, rerun the allocation without due date check
|
||||
// to distribute the in advance portions
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
ignoreDueDateCheck = true;
|
||||
}
|
||||
rerun = !rerun;
|
||||
} while (ignoreDueDateCheck && rerun);
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
}
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* For late repayments
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money latePayment,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, latePayment, transactionMappings,
|
||||
charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For normal on-time repayments
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money onTimePayment,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
|
||||
final MonetaryCurrency currency = onTimePayment.getCurrency();
|
||||
Money transactionAmountRemaining = onTimePayment;
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltyChargesPortion = Money.zero(currency);
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else {
|
||||
Money subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subPenaltyPortion);
|
||||
penaltyChargesPortion = penaltyChargesPortion.add(subPenaltyPortion);
|
||||
|
||||
Money subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subInterestPortion);
|
||||
interestPortion = interestPortion.add(subInterestPortion);
|
||||
|
||||
Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subPrincipalPortion);
|
||||
principalPortion = principalPortion.add(subPrincipalPortion);
|
||||
|
||||
Money subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion);
|
||||
feeChargesPortion = feeChargesPortion.add(subFeePortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
}
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltyChargesPortion = Money.zero(currency);
|
||||
|
||||
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
/**
|
||||
* This {@link LoanRepaymentScheduleTransactionProcessor} defaults to having the payment order of Interest first, then
|
||||
* principal, penalties and fees.
|
||||
*/
|
||||
public class EarlyPaymentLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public static final String STRATEGY_CODE = "early-repayment-strategy";
|
||||
|
||||
public static final String STRATEGY_NAME = "Early Repayment Strategy";
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return STRATEGY_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return STRATEGY_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* For early/'in advance' repayments, pay off in the same way as on-time payments, interest first, principal,
|
||||
* penalties and charges.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Override
|
||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
|
||||
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = paymentInAdvance.getCurrency();
|
||||
Money transactionAmountRemaining = paymentInAdvance;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else {
|
||||
|
||||
// Only allocate to principal:
|
||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
}
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* For late repayments, pay off in the same way as on-time payments, interest first then principal.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Override
|
||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
|
||||
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
|
||||
transactionMappings, charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For normal on-time repayments, pays off interest first, then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
// zero this type of transaction and ignore it for now.
|
||||
transactionAmountRemaining = Money.zero(currency);
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else {
|
||||
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
}
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
// final MonetaryCurrency currency =
|
||||
// transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
/**
|
||||
* Old style {@link LoanRepaymentScheduleTransactionProcessor}.
|
||||
*
|
||||
* For ALL types of transactions, pays off components in order of interest, then principal.
|
||||
*
|
||||
* Other formulas exist on fineract where you can choose 'Declining-Balance Interest Recalculation' which simply means,
|
||||
* recalculate the interest component based on the how much principal is outstanding at a point in time; but this isnt
|
||||
* trying to model that option only the basic one for now.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class FineractStyleLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public static final String STRATEGY_CODE = "mifos-standard-strategy";
|
||||
|
||||
public static final String STRATEGY_NAME = "Penalties, Fees, Interest, Principal order";
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return STRATEGY_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return STRATEGY_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* For early/'in advance' repayments, pay off in the same way as on-time payments, interest first then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
|
||||
charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For late repayments, pay off in the same way as on-time payments, interest first then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
|
||||
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
|
||||
transactionMappings, charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For normal on-time repayments, pays off interest first, then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
}
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltyChargesPortion = Money.zero(currency);
|
||||
|
||||
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
/**
|
||||
* Heavensfamily style {@link LoanRepaymentScheduleTransactionProcessor}.
|
||||
*
|
||||
* For standard transactions, pays off components in order of interest, then principal.
|
||||
*
|
||||
* If a transaction results in an advance payment or overpayment for a given installment, the over paid amount is pay
|
||||
* off on the principal component of subsequent installments.
|
||||
*
|
||||
* If the entire principal of an installment is paid in advance then the interest component is waived.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public static final String STRATEGY_CODE = "heavensfamily-strategy";
|
||||
|
||||
public static final String STRATEGY_NAME = "HeavensFamily Unique";
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return STRATEGY_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return STRATEGY_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* For late repayments, pay off in the same way as on-time payments, interest first then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
|
||||
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
|
||||
transactionMappings, charges);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate) {
|
||||
|
||||
boolean isInAdvance = false;
|
||||
|
||||
LocalDate lastInstallmentDueDate = null;
|
||||
int previousInstallmentIndex = 0;
|
||||
if (currentInstallmentIndex > 0) {
|
||||
previousInstallmentIndex = currentInstallmentIndex - 1;
|
||||
}
|
||||
|
||||
final LoanRepaymentScheduleInstallment previousInstallment = installments.get(previousInstallmentIndex);
|
||||
lastInstallmentDueDate = previousInstallment.getDueDate();
|
||||
|
||||
return DateUtils.isBefore(transactionDate, lastInstallmentDueDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* For early/'in advance' repayments, pays off principal component only.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
|
||||
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = paymentInAdvance.getCurrency();
|
||||
Money transactionAmountRemaining = paymentInAdvance;
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltyChargesPortion = Money.zero(currency);
|
||||
|
||||
if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
} else {
|
||||
|
||||
if (currentInstallment.isPrincipalNotCompleted(currency)) {
|
||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
if (currentInstallment.isPrincipalCompleted(currency)) {
|
||||
// FIXME - KW - if auto waiving interest need to create
|
||||
// another transaction to handle this.
|
||||
currentInstallment.waiveInterestComponent(transactionDate, currentInstallment.getInterestCharged(currency));
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
}
|
||||
|
||||
// 1. pay of principal with over payment.
|
||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* For normal on-time repayments, pays off interest first, then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
} else {
|
||||
// 1. pay of principal before interest.
|
||||
|
||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
/**
|
||||
* This {@link LoanRepaymentScheduleTransactionProcessor} defaults to having the payment order of Interest first, then
|
||||
* principal, penalties and fees.
|
||||
*/
|
||||
public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor
|
||||
extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public static final String STRATEGY_CODE = "interest-principal-penalties-fees-order-strategy";
|
||||
|
||||
public static final String STRATEGY_NAME = "Interest, Principal, Penalties, Fees Order";
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return STRATEGY_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return STRATEGY_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* For early/'in advance' repayments, pay off in the same way as on-time payments, interest first, principal,
|
||||
* penalties and charges.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Override
|
||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
|
||||
charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For late repayments, pay off in the same way as on-time payments, interest first then principal.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Override
|
||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
|
||||
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
|
||||
transactionMappings, charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For normal on-time repayments, pays off interest first, then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else {
|
||||
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
}
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInterestFirstRepaymentScheduleTransactionProcessor() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
/**
|
||||
* This {@link LoanRepaymentScheduleTransactionProcessor} defaults to having the payment order of principal first, then
|
||||
* interest, penalties and fees.
|
||||
*/
|
||||
public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor
|
||||
extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public static final String STRATEGY_CODE = "principal-interest-penalties-fees-order-strategy";
|
||||
|
||||
public static final String STRATEGY_NAME = "Principal, Interest, Penalties, Fees Order";
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return STRATEGY_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return STRATEGY_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* For early/'in advance' repayments, pay off in the same way as on-time payments, interest first, principal,
|
||||
* penalties and charges.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Override
|
||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
|
||||
charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For late repayments, pay off in the same way as on-time payments, interest first then principal.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Override
|
||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
|
||||
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
|
||||
transactionMappings, charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For normal on-time repayments, pays off interest first, then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else {
|
||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
}
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
// final MonetaryCurrency currency =
|
||||
// transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
/**
|
||||
* Adhikar/RBI style {@link LoanRepaymentScheduleTransactionProcessor}.
|
||||
*
|
||||
* From https://mifosforge.jira.com/browse/MIFOS-5636:
|
||||
*
|
||||
* Per RBI regulations, all interest must be paid (both current and overdue) before principal is paid.
|
||||
*
|
||||
* For example on a loan with two installments due (one current and one overdue) of 220 each (200 principal + 20
|
||||
* interest):
|
||||
*
|
||||
* Partial Payment of 40 20 Payment to interest on Installment #1 (200 principal remaining) 20 Payment to interest on
|
||||
* Installment #2 (200 principal remaining)
|
||||
*/
|
||||
public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public static final String STRATEGY_CODE = "rbi-india-strategy";
|
||||
|
||||
public static final String STRATEGY_NAME = "Overdue/Due Fee/Int,Principal";
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return STRATEGY_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return STRATEGY_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* For early/'in advance' repayments, pays off principal component only.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Override
|
||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
|
||||
charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For late repayments, pay off in the same way as on-time payments, interest first then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
|
||||
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Set<LoanCharge> charges) {
|
||||
|
||||
// pay of overdue and current interest due given transaction date
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money interestWaivedPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltyChargesPortion = Money.zero(currency);
|
||||
|
||||
if (loanTransaction.isInterestWaiver()) {
|
||||
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
|
||||
|
||||
final Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
loanTransaction.updateComponents(principalPortion, interestWaivedPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (interestWaivedPortion.isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestWaivedPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
final Money principalPortion = Money.zero(currency);
|
||||
final Money interestPortion = Money.zero(currency);
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
} else {
|
||||
|
||||
final LoanRepaymentScheduleInstallment currentInstallmentBasedOnTransactionDate = nearestInstallment(
|
||||
loanTransaction.getTransactionDate(), installments);
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
if ((installment.isInterestDue(currency) || installment.getFeeChargesOutstanding(currency).isGreaterThanZero()
|
||||
|| installment.getPenaltyChargesOutstanding(currency).isGreaterThanZero())
|
||||
&& (installment.isOverdueOn(loanTransaction.getTransactionDate()) || installment.getInstallmentNumber()
|
||||
.equals(currentInstallmentBasedOnTransactionDate.getInstallmentNumber()))) {
|
||||
penaltyChargesPortion = installment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = installment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
final Money interestPortion = installment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
final Money principalPortion = Money.zero(currency);
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, installment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// With whatever is remaining, pay off principal components of
|
||||
// installments
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
if (installment.isPrincipalNotCompleted(currency) && transactionAmountRemaining.isGreaterThanZero()) {
|
||||
final Money principalPortion = installment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
final Money interestPortion = Money.zero(currency);
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, Money.zero(currency), Money.zero(currency));
|
||||
boolean isMappingUpdated = false;
|
||||
for (LoanTransactionToRepaymentScheduleMapping repaymentScheduleMapping : transactionMappings) {
|
||||
if (repaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getDueDate().equals(installment.getDueDate())) {
|
||||
repaymentScheduleMapping.updateComponents(principalPortion, principalPortion.zero(), principalPortion.zero(),
|
||||
principalPortion.zero());
|
||||
isMappingUpdated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isMappingUpdated && principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion)
|
||||
.isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, installment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
private LoanRepaymentScheduleInstallment nearestInstallment(final LocalDate transactionDate,
|
||||
final List<LoanRepaymentScheduleInstallment> installments) {
|
||||
LoanRepaymentScheduleInstallment nearest = installments.get(0); // installments must be sorted by dates
|
||||
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||
if (DateUtils.isBefore(transactionDate, installment.getDueDate())) {
|
||||
break;
|
||||
}
|
||||
nearest = installment;
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* For normal on-time repayments, pays off interest first, then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
} else {
|
||||
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInterestFirstRepaymentScheduleTransactionProcessor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
// final MonetaryCurrency currency =
|
||||
// transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
}
|
||||
+9
-5
@@ -219,6 +219,7 @@ public final class LoanApplicationTerms {
|
||||
private boolean isScheduleExtensionForDownPaymentDisabled;
|
||||
private Money disbursedPrincipal;
|
||||
private final LoanScheduleType loanScheduleType;
|
||||
private final LoanScheduleProcessingType loanScheduleProcessingType;
|
||||
|
||||
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency,
|
||||
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
|
||||
@@ -246,7 +247,7 @@ public final class LoanApplicationTerms {
|
||||
final Boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
|
||||
final Boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType,
|
||||
final LocalDate submittedOnDate, final Boolean isScheduleExtensionForDownPaymentDisabled,
|
||||
final LoanScheduleType loanScheduleType) {
|
||||
final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
|
||||
|
||||
final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
|
||||
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
|
||||
@@ -265,7 +266,7 @@ public final class LoanApplicationTerms {
|
||||
isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
|
||||
isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment,
|
||||
isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled,
|
||||
loanScheduleType);
|
||||
loanScheduleType, loanScheduleProcessingType);
|
||||
|
||||
}
|
||||
|
||||
@@ -322,6 +323,7 @@ public final class LoanApplicationTerms {
|
||||
isScheduleExtensionForDownPaymentDisabled = loanProductRelatedDetail.isDisableScheduleExtensionForDownPayment();
|
||||
}
|
||||
LoanScheduleType loanScheduleType = loanProductRelatedDetail.getLoanScheduleType();
|
||||
LoanScheduleProcessingType loanScheduleProcessingType = loanProductRelatedDetail.getLoanScheduleProcessingType();
|
||||
return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
|
||||
repaymentEvery, repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod,
|
||||
interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate,
|
||||
@@ -338,7 +340,7 @@ public final class LoanApplicationTerms {
|
||||
isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
|
||||
isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment,
|
||||
isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled,
|
||||
loanScheduleType);
|
||||
loanScheduleType, loanScheduleProcessingType);
|
||||
}
|
||||
|
||||
private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
|
||||
@@ -367,7 +369,8 @@ public final class LoanApplicationTerms {
|
||||
final boolean isPrincipalCompoundingDisabledForOverdueLoans, final boolean isDownPaymentEnabled,
|
||||
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean isAutoRepaymentForDownPaymentEnabled,
|
||||
final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate,
|
||||
final boolean isScheduleExtensionForDownPaymentDisabled, final LoanScheduleType loanScheduleType) {
|
||||
final boolean isScheduleExtensionForDownPaymentDisabled, final LoanScheduleType loanScheduleType,
|
||||
final LoanScheduleProcessingType loanScheduleProcessingType) {
|
||||
|
||||
this.currency = currency;
|
||||
this.loanTermFrequency = loanTermFrequency;
|
||||
@@ -452,6 +455,7 @@ public final class LoanApplicationTerms {
|
||||
this.submittedOnDate = submittedOnDate;
|
||||
this.isScheduleExtensionForDownPaymentDisabled = isScheduleExtensionForDownPaymentDisabled;
|
||||
this.loanScheduleType = loanScheduleType;
|
||||
this.loanScheduleProcessingType = loanScheduleProcessingType;
|
||||
}
|
||||
|
||||
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
|
||||
@@ -1295,7 +1299,7 @@ public final class LoanApplicationTerms {
|
||||
this.graceOnArrearsAgeing, this.daysInMonthType.getValue(), this.daysInYearType.getValue(),
|
||||
this.interestRecalculationEnabled, this.isEqualAmortization, this.isDownPaymentEnabled,
|
||||
this.disbursedAmountPercentageForDownPayment, this.isAutoRepaymentForDownPaymentEnabled,
|
||||
this.isScheduleExtensionForDownPaymentDisabled, this.loanScheduleType);
|
||||
this.isScheduleExtensionForDownPaymentDisabled, this.loanScheduleType, this.loanScheduleProcessingType);
|
||||
}
|
||||
|
||||
public Integer getLoanTermFrequency() {
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum LoanScheduleProcessingType {
|
||||
|
||||
HORIZONTAL("Horizontal"), VERTICAL("Vertical");
|
||||
|
||||
private final String humanReadableName;
|
||||
|
||||
public static List<EnumOptionData> getValuesAsEnumOptionDataList() {
|
||||
return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList();
|
||||
}
|
||||
|
||||
public EnumOptionData asEnumOptionData() {
|
||||
return new EnumOptionData((long) this.ordinal(), this.name(), this.humanReadableName);
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -18,6 +18,25 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum LoanScheduleType {
|
||||
CUMULATIVE, PROGRESSIVE
|
||||
|
||||
CUMULATIVE("Cumulative"), PROGRESSIVE("Progressive");
|
||||
|
||||
private final String humanReadableName;
|
||||
|
||||
public static List<EnumOptionData> getValuesAsEnumOptionDataList() {
|
||||
return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList();
|
||||
}
|
||||
|
||||
public EnumOptionData asEnumOptionData() {
|
||||
return new EnumOptionData((long) this.ordinal(), this.name(), this.humanReadableName);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -158,5 +158,6 @@ public interface LoanProductConstants {
|
||||
|
||||
// loan schedule type
|
||||
String LOAN_SCHEDULE_TYPE = "loanScheduleType";
|
||||
String LOAN_SCHEDULE_PROCESSING_TYPE = "loanScheduleProcessingType";
|
||||
|
||||
}
|
||||
|
||||
+29
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanproduct.domain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.IntStream;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
@@ -72,6 +73,34 @@ public class AdvancedPaymentAllocationsValidator {
|
||||
}
|
||||
}
|
||||
|
||||
public void checkGroupingOfAllocationRules(List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules) {
|
||||
loanProductPaymentAllocationRules.forEach(paymentAllocationRule -> {
|
||||
AtomicInteger pastDueRuleInteger = new AtomicInteger();
|
||||
AtomicInteger dueRuleInteger = new AtomicInteger();
|
||||
AtomicInteger inAdvanceRuleInteger = new AtomicInteger();
|
||||
paymentAllocationRule.getAllocationTypes().forEach(paymentAllocationType -> {
|
||||
validateAllocationType(paymentAllocationType, pastDueRuleInteger, dueRuleInteger, inAdvanceRuleInteger);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void validateAllocationType(PaymentAllocationType paymentAllocationType, AtomicInteger pastDueRuleInteger,
|
||||
AtomicInteger dueRuleInteger, AtomicInteger inAdvanceRuleInteger) {
|
||||
switch (paymentAllocationType.getDueType()) {
|
||||
case PAST_DUE -> validateDueType(pastDueRuleInteger, dueRuleInteger, inAdvanceRuleInteger);
|
||||
case DUE -> validateDueType(dueRuleInteger, pastDueRuleInteger, inAdvanceRuleInteger);
|
||||
case IN_ADVANCE -> validateDueType(inAdvanceRuleInteger, pastDueRuleInteger, dueRuleInteger);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDueType(AtomicInteger currentRuleInteger, AtomicInteger otherRule1Integer, AtomicInteger otherRule2Integer) {
|
||||
currentRuleInteger.incrementAndGet();
|
||||
if ((otherRule1Integer.get() > 0 && otherRule1Integer.get() < 4) || (otherRule2Integer.get() > 0 && otherRule2Integer.get() < 4)) {
|
||||
raiseValidationError("mixed.due.type.allocation.rules.are.not.supported.with.horizontal.installment.processing",
|
||||
"Horizontal repayment schedule processing is not supporting mixed due type allocation rules!");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasDuplicateTransactionTypes(List<LoanProductPaymentAllocationRule> rules) {
|
||||
return rules != null
|
||||
&& rules.stream().map(LoanProductPaymentAllocationRule::getTransactionType).distinct().toList().size() != rules.size();
|
||||
|
||||
+13
-3
@@ -62,6 +62,7 @@ import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData;
|
||||
import org.apache.fineract.portfolio.floatingrates.domain.FloatingRate;
|
||||
import org.apache.fineract.portfolio.fund.domain.Fund;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
|
||||
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException;
|
||||
@@ -312,6 +313,15 @@ public class LoanProduct extends AbstractPersistableCustom {
|
||||
loanScheduleType = LoanScheduleType.CUMULATIVE;
|
||||
}
|
||||
|
||||
final LoanScheduleProcessingType loanScheduleProcessingType;
|
||||
if (LoanScheduleType.PROGRESSIVE.equals(loanScheduleType) && command.hasParameter("loanScheduleProcessingType")) {
|
||||
loanScheduleProcessingType = LoanScheduleProcessingType
|
||||
.valueOf(command.stringValueOfParameterNamed("loanScheduleProcessingType"));
|
||||
} else {
|
||||
// For backward compatibility
|
||||
loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL;
|
||||
}
|
||||
|
||||
final boolean useBorrowerCycle = command
|
||||
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME);
|
||||
final Set<LoanProductBorrowerCycleVariations> loanProductBorrowerCycleVariations = new HashSet<>();
|
||||
@@ -436,7 +446,7 @@ public class LoanProduct extends AbstractPersistableCustom {
|
||||
disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber,
|
||||
dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment,
|
||||
enableAutoRepaymentForDownPayment, repaymentStartDateType, disableScheduleExtensionForDownPayment,
|
||||
enableInstallmentLevelDelinquency, loanScheduleType);
|
||||
enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType);
|
||||
|
||||
}
|
||||
|
||||
@@ -652,7 +662,7 @@ public class LoanProduct extends AbstractPersistableCustom {
|
||||
final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
|
||||
final boolean enableAutoRepaymentForDownPayment, final RepaymentStartDateType repaymentStartDateType,
|
||||
final boolean disableScheduleExtensionForDownPayment, final boolean enableInstallmentLevelDelinquency,
|
||||
final LoanScheduleType loanScheduleType) {
|
||||
final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
|
||||
this.fund = fund;
|
||||
this.transactionProcessingStrategyCode = transactionProcessingStrategyCode;
|
||||
|
||||
@@ -694,7 +704,7 @@ public class LoanProduct extends AbstractPersistableCustom {
|
||||
recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod,
|
||||
inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType.getValue(), daysInYearType.getValue(),
|
||||
isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment,
|
||||
enableAutoRepaymentForDownPayment, disableScheduleExtensionForDownPayment, loanScheduleType);
|
||||
enableAutoRepaymentForDownPayment, disableScheduleExtensionForDownPayment, loanScheduleType, loanScheduleProcessingType);
|
||||
|
||||
this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings();
|
||||
|
||||
|
||||
+13
-3
@@ -39,6 +39,7 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType;
|
||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
|
||||
|
||||
@@ -146,6 +147,10 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
@Enumerated(EnumType.STRING)
|
||||
private LoanScheduleType loanScheduleType;
|
||||
|
||||
@Column(name = "loan_schedule_processing_type", nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private LoanScheduleProcessingType loanScheduleProcessingType;
|
||||
|
||||
public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currency, final BigDecimal principal,
|
||||
final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType,
|
||||
final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod,
|
||||
@@ -157,7 +162,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization,
|
||||
final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
|
||||
final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment,
|
||||
final LoanScheduleType loanScheduleType) {
|
||||
final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
|
||||
|
||||
return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType,
|
||||
nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion,
|
||||
@@ -165,7 +170,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod,
|
||||
inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
|
||||
isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment,
|
||||
disableScheduleExtensionForDownPayment, loanScheduleType);
|
||||
disableScheduleExtensionForDownPayment, loanScheduleType, loanScheduleProcessingType);
|
||||
}
|
||||
|
||||
protected LoanProductRelatedDetail() {
|
||||
@@ -183,7 +188,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization,
|
||||
final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
|
||||
final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment,
|
||||
final LoanScheduleType loanScheduleType) {
|
||||
final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
|
||||
this.currency = currency;
|
||||
this.principal = defaultPrincipal;
|
||||
this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod;
|
||||
@@ -215,6 +220,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment;
|
||||
this.disableScheduleExtensionForDownPayment = disableScheduleExtensionForDownPayment;
|
||||
this.loanScheduleType = loanScheduleType;
|
||||
this.loanScheduleProcessingType = loanScheduleProcessingType;
|
||||
}
|
||||
|
||||
private Integer defaultToNullIfZero(final Integer value) {
|
||||
@@ -740,4 +746,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
public LoanScheduleType getLoanScheduleType() {
|
||||
return loanScheduleType;
|
||||
}
|
||||
|
||||
public LoanScheduleProcessingType getLoanScheduleProcessingType() {
|
||||
return loanScheduleProcessingType;
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -34,4 +34,5 @@
|
||||
<include relativeToChangelogFile="true" file="parts/1009_refactor_loan_Installment_delinquency_tag.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/1010_introduce_loan_schedule_type_configuration.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/1011_add_delinquency_actions_table.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/1012_introduce_loan_schedule_processing_type_configuration.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<addColumn tableName="m_product_loan">
|
||||
<column defaultValue="HORIZONTAL" type="varchar(20)" name="loan_schedule_processing_type">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<addColumn tableName="m_loan">
|
||||
<column defaultValue="HORIZONTAL" type="varchar(20)" name="loan_schedule_processing_type">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+51
-9
@@ -143,11 +143,37 @@ class AdvancedPaymentAllocationsValidatorTest {
|
||||
() -> underTest.validate(List.of(lppr1, lppr2), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkGroupingOfAllocationRulesGoodOrder() {
|
||||
List<LoanProductPaymentAllocationRule> allocationRules1 = List.of(createLoanProductAllocationRule1());
|
||||
Assertions.assertDoesNotThrow(() -> underTest.checkGroupingOfAllocationRules(allocationRules1));
|
||||
|
||||
List<LoanProductPaymentAllocationRule> allocationRules2 = List.of(createLoanProductAllocationRule4());
|
||||
Assertions.assertDoesNotThrow(() -> underTest.checkGroupingOfAllocationRules(allocationRules2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkGroupingOfAllocationRulesWrongOrder() {
|
||||
List<LoanProductPaymentAllocationRule> allocationRules = List.of(createLoanProductAllocationRule3());
|
||||
assertPlatformValidationException("Horizontal repayment schedule processing is not supporting mixed due type allocation rules!",
|
||||
"mixed.due.type.allocation.rules.are.not.supported.with.horizontal.installment.processing",
|
||||
() -> underTest.checkGroupingOfAllocationRules(allocationRules));
|
||||
}
|
||||
|
||||
private void assertPlatformValidationException(String message, String code, Executable executable) {
|
||||
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, executable);
|
||||
assertPlatformException(message, code, validationException);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule1() {
|
||||
LoanProductPaymentAllocationRule lppr1 = new LoanProductPaymentAllocationRule();
|
||||
lppr1.setTransactionType(DEFAULT);
|
||||
lppr1.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
|
||||
lppr1.setAllocationTypes(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
|
||||
return lppr1;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule2() {
|
||||
LoanProductPaymentAllocationRule lppr2 = new LoanProductPaymentAllocationRule();
|
||||
@@ -160,20 +186,36 @@ class AdvancedPaymentAllocationsValidatorTest {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule1() {
|
||||
LoanProductPaymentAllocationRule lppr1 = new LoanProductPaymentAllocationRule();
|
||||
lppr1.setTransactionType(DEFAULT);
|
||||
lppr1.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
|
||||
lppr1.setAllocationTypes(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
|
||||
return lppr1;
|
||||
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule3() {
|
||||
LoanProductPaymentAllocationRule lppr = new LoanProductPaymentAllocationRule();
|
||||
lppr.setTransactionType(REPAYMENT);
|
||||
lppr.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
|
||||
ArrayList<PaymentAllocationType> allocationTypes = new ArrayList<>(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
|
||||
Collections.swap(allocationTypes, 0, 4);
|
||||
Collections.swap(allocationTypes, 3, 8);
|
||||
Collections.swap(allocationTypes, 7, 11);
|
||||
lppr.setAllocationTypes(allocationTypes);
|
||||
return lppr;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule4() {
|
||||
LoanProductPaymentAllocationRule lppr = new LoanProductPaymentAllocationRule();
|
||||
lppr.setTransactionType(DEFAULT);
|
||||
lppr.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
|
||||
ArrayList<PaymentAllocationType> allocationTypes = new ArrayList<>(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
|
||||
Collections.swap(allocationTypes, 0, 4);
|
||||
Collections.swap(allocationTypes, 1, 5);
|
||||
Collections.swap(allocationTypes, 2, 6);
|
||||
Collections.swap(allocationTypes, 3, 7);
|
||||
lppr.setAllocationTypes(allocationTypes);
|
||||
return lppr;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<Pair<Integer, PaymentAllocationType>> createPaymentAllocationTypeList() {
|
||||
AtomicInteger i = new AtomicInteger(1);
|
||||
List<Pair<Integer, PaymentAllocationType>> list = EnumSet.allOf(PaymentAllocationType.class).stream()
|
||||
.map(p -> Pair.of(i.getAndIncrement(), p)).toList();
|
||||
return list;
|
||||
return EnumSet.allOf(PaymentAllocationType.class).stream().map(p -> Pair.of(i.getAndIncrement(), p)).toList();
|
||||
}
|
||||
|
||||
private void assertPlatformException(String expectedMessage, String expectedCode,
|
||||
|
||||
Reference in New Issue
Block a user