blob: b06275bad5a270d85e32319eb081dd26c27c8dec [file] [log] [blame]
/**
* 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.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
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.data.LoanChargePaidDetail;
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.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.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;
import org.joda.time.LocalDate;
/**
* 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 {
/**
* Provides support for passing all {@link LoanTransaction}'s so it will
* completely re-process the entire loan schedule. This is required in cases
* where the {@link LoanTransaction} being processed is in the past and
* falls before existing transactions or and adjustment is made to an
* existing in which case the entire loan schedule needs to be re-processed.
*/
@Override
public ChangedTransactionDetail handleTransaction(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) {
if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate())) {
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) {
if (!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) {
final Comparator<LoanRepaymentScheduleInstallment> byDate = new Comparator<LoanRepaymentScheduleInstallment>() {
@Override
public int compare(LoanRepaymentScheduleInstallment ord1, LoanRepaymentScheduleInstallment ord2) {
return ord1.getDueDate().compareTo(ord2.getDueDate());
}
};
Collections.sort(installments, byDate);
}
if (loanTransaction.isRepayment() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
// pass through for new transactions
if (loanTransaction.getId() == null) {
handleTransaction(loanTransaction, currency, installments, charges);
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
handleTransaction(newLoanTransaction, currency, installments, charges);
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 {
loanTransaction.reverse();
loanTransaction.updateExternalId(null);
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction);
}
}
} else if (loanTransaction.isWriteOff()) {
loanTransaction.resetDerivedComponents();
handleWriteOff(loanTransaction, currency, installments);
} else if (loanTransaction.isRefundForActiveLoan()) {
loanTransaction.resetDerivedComponents();
handleRefund(loanTransaction, currency, installments, charges);
}
}
return changedTransactionDetail;
}
/**
* Provides support for processing the latest transaction (which should be
* latest transaction) against the loan schedule.
*/
@Override
public void handleTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) {
final Money amountToProcess = null;
final boolean isChargeAmount = false;
handleTransaction(loanTransaction, currency, installments, charges, amountToProcess, isChargeAmount);
}
private void handleTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, final Money chargeAmountToProcess,
final boolean isFeeCharge) {
Money transactionAmountUnprocessed = handleTransactionAndCharges(loanTransaction, currency, installments, charges,
chargeAmountToProcess, isFeeCharge);
if (transactionAmountUnprocessed.isGreaterThanZero()) {
if (loanTransaction.isWaiver()) {
loanTransaction.updateComponentsAndTotal(transactionAmountUnprocessed.zero(), transactionAmountUnprocessed.zero(),
transactionAmountUnprocessed.zero(), transactionAmountUnprocessed.zero());
} else {
onLoanOverpayment(loanTransaction, transactionAmountUnprocessed);
loanTransaction.updateOverPayments(transactionAmountUnprocessed);
}
}
}
private Money handleTransactionAndCharges(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, final Money chargeAmountToProcess,
final boolean isFeeCharge) {
// to.
if (loanTransaction.isRepayment() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
loanTransaction.resetDerivedComponents();
}
Money transactionAmountUnprocessed = processTransaction(loanTransaction, currency, installments, 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()) {
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;
}
private Money processTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, 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()) {
// is this transaction early/late/on-time with respect to
// the
// current installment?
if (isTransactionInAdvanceOfInstallment(installmentIndex, installments, transactionDate, transactionAmountUnprocessed)) {
transactionAmountUnprocessed = handleTransactionThatIsPaymentInAdvanceOfInstallment(currentInstallment,
installments, loanTransaction, transactionDate, transactionAmountUnprocessed, transactionMappings);
} else if (isTransactionALateRepaymentOnInstallment(installmentIndex, installments,
loanTransaction.getTransactionDate())) {
// does this result in a late payment of existing
// installment?
transactionAmountUnprocessed = handleTransactionThatIsALateRepaymentOfInstallment(currentInstallment, installments,
loanTransaction, transactionAmountUnprocessed, transactionMappings);
} else {
// standard transaction
transactionAmountUnprocessed = handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment,
loanTransaction, transactionAmountUnprocessed, transactionMappings);
}
}
}
installmentIndex++;
}
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
return transactionAmountUnprocessed;
}
private 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;
}
private 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;
}
private 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);
}
}
}
private 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
|| chargePerInstallment.getRepaymentInstallment().getDueDate()
.isAfter(unpaidLoanChargePerInstallment.getRepaymentInstallment().getDueDate())) {
installemntCharge = loanCharge;
chargePerInstallment = unpaidLoanChargePerInstallment;
}
} else if (earliestUnpaidCharge == null || loanCharge.getDueLocalDate().isBefore(earliestUnpaidCharge.getDueLocalDate())) {
earliestUnpaidCharge = loanCharge;
}
}
}
if (earliestUnpaidCharge == null
|| (chargePerInstallment != null && earliestUnpaidCharge.getDueLocalDate().isAfter(
chargePerInstallment.getRepaymentInstallment().getDueDate()))) {
earliestUnpaidCharge = installemntCharge;
}
return earliestUnpaidCharge;
}
@Override
public 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);
}
// abstract interface
/**
* 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 transactionDate.isAfter(currentInstallment.getDueDate());
}
/**
* For late repayments, how should components of installment be paid off
*
* @param transactionMappings
* TODO
*/
protected abstract Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed, final 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 currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate,
@SuppressWarnings("unused") final Money transactionAmount) {
final LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
return transactionDate.isBefore(currentInstallment.getDueDate());
}
/**
* For early/'in advance' repayments.
*
* @param transactionMappings
* TODO
*/
protected abstract Money handleTransactionThatIsPaymentInAdvanceOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment, final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction, final LocalDate transactionDate, final Money paymentInAdvance,
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings);
/**
* For normal on-time repayments.
*
* @param transactionMappings
* TODO
*/
protected abstract Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings);
/**
* 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.
}
@Override
public Money handleRepaymentSchedule(final List<LoanTransaction> transactionsPostDisbursement, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments) {
Money unProcessed = Money.zero(currency);
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
Money amountToProcess = null;
if (loanTransaction.isRepayment() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
loanTransaction.resetDerivedComponents();
}
unProcessed = processTransaction(loanTransaction, currency, installments, amountToProcess);
}
return unProcessed;
}
@Override
public boolean isInterestFirstRepaymentScheduleTransactionProcessor() {
return false;
}
@Override
public void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) {
// TODO Auto-generated method stub
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
final Comparator<LoanRepaymentScheduleInstallment> byDate = new Comparator<LoanRepaymentScheduleInstallment>() {
@Override
public int compare(LoanRepaymentScheduleInstallment ord1, LoanRepaymentScheduleInstallment ord2) {
return ord1.getDueDate().compareTo(ord2.getDueDate());
}
};
Collections.sort(installments, 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);
}
/**
* 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(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings);
private 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 && chargePerInstallment.getRepaymentInstallment().getDueDate()
.isBefore(paidLoanChargePerInstallment.getRepaymentInstallment().getDueDate()))) {
installemntCharge = loanCharge;
chargePerInstallment = paidLoanChargePerInstallment;
}
} else if (latestPaidCharge == null || (loanCharge.isPaidOrPartiallyPaid(currency))
&& loanCharge.getDueLocalDate().isAfter(latestPaidCharge.getDueLocalDate())) {
latestPaidCharge = loanCharge;
}
}
}
if (latestPaidCharge == null
|| (chargePerInstallment != null && latestPaidCharge.getDueLocalDate().isAfter(
chargePerInstallment.getRepaymentInstallment().getDueDate()))) {
latestPaidCharge = installemntCharge;
}
return latestPaidCharge;
}
}