FINERACT-1905: New repayment strategy

This commit is contained in:
Adam Saghy
2023-03-27 14:12:34 +02:00
committed by Arnold Gálovics
parent da810378f3
commit 24bc7bb5fb
31 changed files with 1316 additions and 91 deletions
@@ -60,6 +60,8 @@ public class FineractProperties {
private FineractNotificationProperties notification;
private FineractLoanProperties loan;
@Getter
@Setter
public static class FineractTenantProperties {
@@ -295,4 +297,33 @@ public class FineractProperties {
private boolean enabled;
}
@Getter
@Setter
public static class FineractLoanProperties {
private FineractTransactionProcessorProperties transactionProcessor;
}
@Getter
@Setter
public static class FineractTransactionProcessorProperties {
private FineractTransactionProcessorItemProperties creocore;
private FineractTransactionProcessorItemProperties earlyRepayment;
private FineractTransactionProcessorItemProperties mifosStandard;
private FineractTransactionProcessorItemProperties heavensFamily;
private FineractTransactionProcessorItemProperties interestPrincipalPenaltiesFees;
private FineractTransactionProcessorItemProperties principalInterestPenaltiesFees;
private FineractTransactionProcessorItemProperties rbiIndia;
private FineractTransactionProcessorItemProperties duePenaltyFeeInterestPrincipalInAdvancePrincipalPenaltyFeeInterest;
private boolean errorNotFoundFail;
}
@Getter
@Setter
public static class FineractTransactionProcessorItemProperties {
private boolean enabled;
}
}
@@ -28,6 +28,7 @@ import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.persistence.CascadeType;
@@ -1006,6 +1007,23 @@ public class LoanCharge extends AbstractPersistableCustom {
return ChargeTimeType.fromInt(this.chargeTime);
}
/**
* Return the effective due date of the loan charge. For installment fee we are using the earliest not fully paid
* installment due date
*
* @return LocalDate
*/
public LocalDate getEffectiveDueDate() {
LocalDate dueDate;
if (Objects.requireNonNull(getChargeTimeType()) == ChargeTimeType.INSTALMENT_FEE) {
LoanInstallmentCharge firstUnpaidInstallment = getUnpaidInstallmentLoanCharge();
dueDate = firstUnpaidInstallment != null ? firstUnpaidInstallment.getInstallment().getDueDate() : null;
} else {
dueDate = getDueLocalDate();
}
return dueDate;
}
public LoanChargeData toData() {
EnumOptionData chargeTimeTypeData = new EnumOptionData((long) getChargeTimeType().ordinal(), getChargeTimeType().getCode(),
String.valueOf(getChargeTimeType().getValue()));
@@ -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());
}
}
@@ -431,7 +431,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
loanTransaction.resetDerivedComponents();
}
Money transactionAmountUnprocessed = processTransaction(loanTransaction, currency, installments, chargeAmountToProcess);
Money transactionAmountUnprocessed = processTransaction(loanTransaction, currency, installments, charges, chargeAmountToProcess);
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
@@ -462,7 +462,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
private Money processTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, Money amountToProcess) {
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, Money amountToProcess) {
int installmentIndex = 0;
final LocalDate transactionDate = loanTransaction.getTransactionDate();
@@ -479,20 +479,18 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
// is this transaction early/late/on-time with respect to
// the
// current installment?
if (isTransactionInAdvanceOfInstallment(installmentIndex, installments, transactionDate,
transactionAmountUnprocessed)) {
if (isTransactionInAdvanceOfInstallment(installmentIndex, installments, transactionDate)) {
transactionAmountUnprocessed = handleTransactionThatIsPaymentInAdvanceOfInstallment(currentInstallment,
installments, loanTransaction, transactionDate, transactionAmountUnprocessed, transactionMappings);
} else if (isTransactionALateRepaymentOnInstallment(installmentIndex, installments,
loanTransaction.getTransactionDate())) {
installments, loanTransaction, transactionAmountUnprocessed, transactionMappings, charges);
} else if (isTransactionALateRepaymentOnInstallment(installmentIndex, installments, transactionDate)) {
// does this result in a late payment of existing
// installment?
transactionAmountUnprocessed = handleTransactionThatIsALateRepaymentOfInstallment(currentInstallment, installments,
loanTransaction, transactionAmountUnprocessed, transactionMappings);
loanTransaction, transactionAmountUnprocessed, transactionMappings, charges);
} else {
// standard transaction
transactionAmountUnprocessed = handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment,
loanTransaction, transactionAmountUnprocessed, transactionMappings);
loanTransaction, transactionAmountUnprocessed, transactionMappings, charges);
}
}
}
@@ -630,10 +628,11 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
*
* @param transactionMappings
* TODO
* @param charges
*/
protected abstract Money handleTransactionThatIsALateRepaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
List<LoanRepaymentScheduleInstallment> installments, LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings);
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges);
/**
* This method is responsible for checking if the current transaction is 'an advance/early payment' based on the
@@ -642,8 +641,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
* 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 List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate) {
final LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
@@ -655,20 +653,22 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
*
* @param transactionMappings
* TODO
* @param charges
*/
protected abstract Money handleTransactionThatIsPaymentInAdvanceOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
List<LoanRepaymentScheduleInstallment> installments, LoanTransaction loanTransaction, LocalDate transactionDate,
Money paymentInAdvance, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings);
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);
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges);
/**
* Invoked when a transaction results in an over-payment of the full loan.
@@ -682,7 +682,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
@Override
public Money handleRepaymentSchedule(final List<LoanTransaction> transactionsPostDisbursement, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments) {
final List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> loanCharges) {
Money unProcessed = Money.zero(currency);
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
Money amountToProcess = null;
@@ -690,9 +690,9 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
loanTransaction.resetDerivedComponents();
}
if (loanTransaction.isInterestWaiver()) {
processTransaction(loanTransaction, currency, installments, amountToProcess);
processTransaction(loanTransaction, currency, installments, loanCharges, amountToProcess);
} else {
unProcessed = processTransaction(loanTransaction, currency, installments, amountToProcess);
unProcessed = processTransaction(loanTransaction, currency, installments, loanCharges, amountToProcess);
}
}
return unProcessed;
@@ -46,7 +46,7 @@ public interface LoanRepaymentScheduleTransactionProcessor {
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments);
Money handleRepaymentSchedule(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments);
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> loanCharges);
/**
* Used in interest recalculation to introduce new interest only installment.
@@ -20,8 +20,10 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
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;
@@ -57,10 +59,9 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractL
/**
* For creocore, early is defined as any date before the installment due date
*/
@SuppressWarnings("unused")
@Override
protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate, final Money transactionAmount) {
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate) {
final LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
@@ -73,12 +74,11 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractL
@SuppressWarnings("unused")
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance,
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance,
transactionMappings);
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
charges);
}
/**
@@ -88,10 +88,11 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractL
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed, final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final Money transactionAmountUnprocessed, final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Set<LoanCharge> charges) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
transactionMappings);
transactionMappings, charges);
}
/**
@@ -100,7 +101,7 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractL
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
@@ -0,0 +1,243 @@
/**
* 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.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 DueDateRespectiveLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
private static final String STRATEGY_CODE = "due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest-strategy";
private 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;
}
@Override
protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate) {
final LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
return transactionDate.isBefore(currentInstallment.getDueDate());
}
/**
* 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) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
charges);
}
/**
* 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 {
boolean ignoreDueDateCheck = false;
boolean rerun = false;
List<LoanCharge> orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isNotFullyPaid)
.filter(loanCharge -> loanCharge.getEffectiveDueDate() == null
|| !loanCharge.getEffectiveDueDate().isAfter(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 = transactionAmountUnprocessed;
}
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 = transactionAmountUnprocessed;
}
subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, calculatedFeeCharge);
transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion);
feeChargesPortion = feeChargesPortion.add(subFeePortion);
if (ignoreDueDateCheck || !transactionDate.isBefore(currentInstallment.getDueDate())) {
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
}
principalPortion = principalPortion
.add(currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining));
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
// 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;
}
@Override
protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {
// TODO - KW - dont do anything with loan over-payment for now
}
@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;
}
}
@@ -20,8 +20,10 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
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;
@@ -55,10 +57,10 @@ public class EarlyPaymentLoanRepaymentScheduleTransactionProcessor extends Abstr
@SuppressWarnings("unused")
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance,
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
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());
@@ -112,10 +114,11 @@ public class EarlyPaymentLoanRepaymentScheduleTransactionProcessor extends Abstr
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Set<LoanCharge> charges) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
transactionMappings);
transactionMappings, charges);
}
/**
@@ -124,7 +127,7 @@ public class EarlyPaymentLoanRepaymentScheduleTransactionProcessor extends Abstr
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
@@ -20,8 +20,10 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
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;
@@ -56,7 +58,7 @@ public class FineractStyleLoanRepaymentScheduleTransactionProcessor extends Abst
@Override
protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate, final Money transactionAmount) {
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate) {
final LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
@@ -68,12 +70,11 @@ public class FineractStyleLoanRepaymentScheduleTransactionProcessor extends Abst
*/
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance,
transactionMappings);
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
charges);
}
/**
@@ -82,10 +83,11 @@ public class FineractStyleLoanRepaymentScheduleTransactionProcessor extends Abst
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Set<LoanCharge> charges) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
transactionMappings);
transactionMappings, charges);
}
/**
@@ -94,7 +96,7 @@ public class FineractStyleLoanRepaymentScheduleTransactionProcessor extends Abst
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
@@ -20,8 +20,10 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
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;
@@ -61,15 +63,16 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Set<LoanCharge> charges) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
transactionMappings);
transactionMappings, charges);
}
@Override
protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate, final Money transactionAmount) {
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate) {
boolean isInAdvance = false;
@@ -92,10 +95,10 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
*/
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance,
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
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);
@@ -158,7 +161,7 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
@@ -20,8 +20,10 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
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;
@@ -56,12 +58,11 @@ public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
@SuppressWarnings("unused")
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance,
transactionMappings);
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
charges);
}
/**
@@ -71,10 +72,11 @@ public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Set<LoanCharge> charges) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
transactionMappings);
transactionMappings, charges);
}
/**
@@ -83,7 +85,7 @@ public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
@@ -20,8 +20,10 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
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;
@@ -56,12 +58,11 @@ public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
@SuppressWarnings("unused")
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance,
transactionMappings);
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
charges);
}
/**
@@ -71,10 +72,11 @@ public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Set<LoanCharge> charges) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
transactionMappings);
transactionMappings, charges);
}
/**
@@ -83,7 +85,7 @@ public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionPr
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
@@ -20,8 +20,10 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
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;
@@ -63,7 +65,7 @@ public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRe
@SuppressWarnings("unused")
@Override
protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate, final Money transactionAmount) {
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate) {
final LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
@@ -76,12 +78,11 @@ public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRe
@SuppressWarnings("unused")
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance,
transactionMappings);
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
charges);
}
/**
@@ -90,7 +91,8 @@ public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRe
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Set<LoanCharge> charges) {
// pay of overdue and current interest due given transaction date
final LocalDate transactionDate = loanTransaction.getTransactionDate();
@@ -203,7 +205,7 @@ public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRe
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
final List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
@@ -505,7 +505,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
// on scheduled installments to identify the
// unprocessed(early payment ) amounts
Money unprocessed = loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(currentTransactions, currency,
scheduleParams.getInstallments());
scheduleParams.getInstallments(), loanCharges);
if (unprocessed.isGreaterThanZero()) {
scheduleParams.reduceOutstandingBalance(unprocessed);
@@ -805,7 +805,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
// on scheduled installments to identify the
// unprocessed(early payment ) amounts
Money unprocessed = scheduleParams.getLoanRepaymentScheduleTransactionProcessor()
.handleRepaymentSchedule(currentTransactions, currency, scheduleParams.getInstallments());
.handleRepaymentSchedule(currentTransactions, currency, scheduleParams.getInstallments(), loanCharges);
if (unprocessed.isGreaterThanZero()) {
if (loanApplicationTerms.getPreClosureInterestCalculationStrategy().calculateTillRestFrequencyEnabled()) {
@@ -850,7 +850,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
if (applicableDate.isBefore(scheduledDueDate)) {
List<LoanTransaction> currentTransactions = createCurrentTransactionList(detail);
Money unprocessed = scheduleParams.getLoanRepaymentScheduleTransactionProcessor()
.handleRepaymentSchedule(currentTransactions, currency, scheduleParams.getInstallments());
.handleRepaymentSchedule(currentTransactions, currency, scheduleParams.getInstallments(), loanCharges);
Money arrears = fetchArrears(loanApplicationTerms, currency, detail.getTransaction());
if (unprocessed.isGreaterThanZero()) {
updateMapWithAmount(scheduleParams.getPrincipalPortionMap(), unprocessed, applicableDate);
@@ -1377,7 +1377,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
startDate = transactionDate;
additionalPeriodsStartDate = startDate;
}
loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(currentTransactions, currency, params.getInstallments());
loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(currentTransactions, currency, params.getInstallments(),
loanCharges);
updateLatePaidAmountsToPrincipalMap(detail.getTransaction(), loanApplicationTerms, currency, holidayDetailDTO, lastRestDate,
params);
updateLatePaymentsToMap(loanApplicationTerms, holidayDetailDTO, currency, params.getLatePaymentMap(), currentDate,
@@ -2323,7 +2324,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
Money unprocessed = updateEarlyPaidAmountsToMap(loanApplicationTerms, holidayDetailDTO,
loanRepaymentScheduleTransactionProcessor, newRepaymentScheduleInstallments, currency, principalPortionMap,
installment, applicableTransactions, actualPrincipalPortion);
installment, applicableTransactions, actualPrincipalPortion, loan.getActiveCharges());
// this block is to adjust the period number based on the
// actual
@@ -2413,7 +2414,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return LoanScheduleDTO.from(retainedInstallments, loanScheduleModelwithPeriodChanges);
}
public List<LoanRepaymentScheduleInstallment> fetchRetainedInstallments(
private List<LoanRepaymentScheduleInstallment> fetchRetainedInstallments(
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, final LocalDate rescheduleFrom,
MonetaryCurrency currency) {
List<LoanRepaymentScheduleInstallment> newRepaymentScheduleInstallments = new ArrayList<>();
@@ -2455,7 +2456,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
final List<LoanRepaymentScheduleInstallment> newRepaymentScheduleInstallments, MonetaryCurrency currency,
final Map<LocalDate, Money> principalPortionMap, LoanRepaymentScheduleInstallment installment,
Collection<RecalculationDetail> applicableTransactions, Money actualPrincipalPortion) {
Collection<RecalculationDetail> applicableTransactions, Money actualPrincipalPortion, Set<LoanCharge> loanCharges) {
Money unprocessed = Money.zero(currency);
Money totalUnprocessed = Money.zero(currency);
for (RecalculationDetail detail : applicableTransactions) {
@@ -2467,7 +2468,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
// on scheduled installments to identify the
// unprocessed(early payment ) amounts
loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(currentTransactions, currency,
newRepaymentScheduleInstallments);
newRepaymentScheduleInstallments, loanCharges);
// Identifies totalEarlyPayment and early paid amount with this
// transaction
@@ -0,0 +1,30 @@
/**
* 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.starter;
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
public class CreocoreLoanRepaymentScheduleTransactionProcessorCondition extends PropertiesCondition {
@Override
protected boolean matches(FineractProperties properties) {
return properties.getLoan().getTransactionProcessor().getCreocore().isEnabled();
}
}
@@ -0,0 +1,31 @@
/**
* 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.starter;
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
public class DueDateRespectiveLoanRepaymentScheduleTransactionProcessorCondition extends PropertiesCondition {
@Override
protected boolean matches(FineractProperties properties) {
return properties.getLoan().getTransactionProcessor().getDuePenaltyFeeInterestPrincipalInAdvancePrincipalPenaltyFeeInterest()
.isEnabled();
}
}
@@ -0,0 +1,30 @@
/**
* 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.starter;
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
public class EarlyRepaymentLoanRepaymentScheduleTransactionProcessorCondition extends PropertiesCondition {
@Override
protected boolean matches(FineractProperties properties) {
return properties.getLoan().getTransactionProcessor().getEarlyRepayment().isEnabled();
}
}
@@ -0,0 +1,30 @@
/**
* 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.starter;
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
public class HeavensFamilyLoanRepaymentScheduleTransactionProcessorCondition extends PropertiesCondition {
@Override
protected boolean matches(FineractProperties properties) {
return properties.getLoan().getTransactionProcessor().getHeavensFamily().isEnabled();
}
}
@@ -0,0 +1,30 @@
/**
* 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.starter;
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
public class InterestPrincipalPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition extends PropertiesCondition {
@Override
protected boolean matches(FineractProperties properties) {
return properties.getLoan().getTransactionProcessor().getInterestPrincipalPenaltiesFees().isEnabled();
}
}
@@ -22,6 +22,7 @@ import java.util.List;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.DueDateRespectiveLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.EarlyPaymentLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
@@ -29,55 +30,61 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LoanAccountAutoStarter {
@Bean
@ConditionalOnProperty("fineract.loan.transactionprocessor.creocore.enabled")
@Conditional(CreocoreLoanRepaymentScheduleTransactionProcessorCondition.class)
public CreocoreLoanRepaymentScheduleTransactionProcessor creocoreLoanRepaymentScheduleTransactionProcessor() {
return new CreocoreLoanRepaymentScheduleTransactionProcessor();
}
@Bean
@ConditionalOnProperty("fineract.loan.transactionprocessor.early-repayment.enabled")
@Conditional(EarlyRepaymentLoanRepaymentScheduleTransactionProcessorCondition.class)
public EarlyPaymentLoanRepaymentScheduleTransactionProcessor earlyPaymentLoanRepaymentScheduleTransactionProcessor() {
return new EarlyPaymentLoanRepaymentScheduleTransactionProcessor();
}
@Bean
@ConditionalOnProperty("fineract.loan.transactionprocessor.mifos-standard.enabled")
@Conditional(MifosStandardLoanRepaymentScheduleTransactionProcessorCondition.class)
public FineractStyleLoanRepaymentScheduleTransactionProcessor fineractStyleLoanRepaymentScheduleTransactionProcessor() {
return new FineractStyleLoanRepaymentScheduleTransactionProcessor();
}
@Bean
@ConditionalOnProperty("fineract.loan.transactionprocessor.heavensfamily.enabled")
@Conditional(HeavensFamilyLoanRepaymentScheduleTransactionProcessorCondition.class)
public HeavensFamilyLoanRepaymentScheduleTransactionProcessor heavensFamilyLoanRepaymentScheduleTransactionProcessor() {
return new HeavensFamilyLoanRepaymentScheduleTransactionProcessor();
}
@Bean
@ConditionalOnProperty("fineract.loan.transactionprocessor.interest-principal-penalties-fees.enabled")
@Conditional(InterestPrincipalPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition.class)
public InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor interestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor() {
return new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor();
}
@Bean
@ConditionalOnProperty("fineract.loan.transactionprocessor.principal-interest-penalties-fees.enabled")
@Conditional(PrincipalInterestPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition.class)
public PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor principalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor() {
return new PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor();
}
@Bean
@ConditionalOnProperty("fineract.loan.transactionprocessor.rbi-india.enabled")
@Conditional(RBIIndiaLoanRepaymentScheduleTransactionProcessorCondition.class)
public RBILoanRepaymentScheduleTransactionProcessor rbiLoanRepaymentScheduleTransactionProcessor() {
return new RBILoanRepaymentScheduleTransactionProcessor();
}
@Bean
@Conditional(DueDateRespectiveLoanRepaymentScheduleTransactionProcessorCondition.class)
public DueDateRespectiveLoanRepaymentScheduleTransactionProcessor dueDateRespectiveTransactionProcessor() {
return new DueDateRespectiveLoanRepaymentScheduleTransactionProcessor();
}
@Bean
@ConditionalOnMissingBean(LoanRepaymentScheduleTransactionProcessorFactory.class)
public LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory(
@@ -0,0 +1,30 @@
/**
* 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.starter;
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
public class MifosStandardLoanRepaymentScheduleTransactionProcessorCondition extends PropertiesCondition {
@Override
protected boolean matches(FineractProperties properties) {
return properties.getLoan().getTransactionProcessor().getMifosStandard().isEnabled();
}
}
@@ -0,0 +1,30 @@
/**
* 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.starter;
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
public class PrincipalInterestPenaltiesFeesLoanRepaymentScheduleTransactionProcessorCondition extends PropertiesCondition {
@Override
protected boolean matches(FineractProperties properties) {
return properties.getLoan().getTransactionProcessor().getPrincipalInterestPenaltiesFees().isEnabled();
}
}
@@ -0,0 +1,30 @@
/**
* 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.starter;
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
public class RBIIndiaLoanRepaymentScheduleTransactionProcessorCondition extends PropertiesCondition {
@Override
protected boolean matches(FineractProperties properties) {
return properties.getLoan().getTransactionProcessor().getRbiIndia().isEnabled();
}
}
@@ -81,6 +81,7 @@ fineract.loan.transactionprocessor.heavensfamily.enabled=${FINERACT_LOAN_TRANSAC
fineract.loan.transactionprocessor.interest-principal-penalties-fees.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_INTEREST_PRINCIPAL_PENALTIES_FEES_ENABLED:true}
fineract.loan.transactionprocessor.principal-interest-penalties-fees.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_PRINCIPAL_INTEREST_PENALTIES_FEES_ENABLED:true}
fineract.loan.transactionprocessor.rbi-india.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_RBI_INDIA_ENABLED:true}
fineract.loan.transactionprocessor.due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_ENABLED:true}
fineract.loan.transactionprocessor.error-not-found-fail=${FINERACT_LOAN_TRANSACTIONPROCESSOR_ERROR_NOT_FOUND_FAIL:true}
fineract.content.regex-whitelist-enabled=${FINERACT_CONTENT_REGEX_WHITELIST_ENABLED:true}
@@ -119,4 +119,5 @@
<include file="parts/0097_update_accounting_summary_table_reports.xml" relativeToChangelogFile="true" />
<include file="parts/0098_update_transaction_summary_table_report.xml" relativeToChangelogFile="true" />
<include file="parts/0099_add_accrual_transaction_external_event_configuration.xml" relativeToChangelogFile="true" />
<include file="parts/0100_new_repayment_strategy.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,42 @@
<?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">
<insert tableName="r_enum_value">
<column name="enum_name" value="loan_transaction_strategy_id"/>
<column name="enum_id" valueNumeric="8"/>
<column name="enum_message_property" value="Due penalty, fee, interest, principal, In advance principal, penalty, fee, interest"/>
<column name="enum_value" value="Due penalty, fee, interest, principal, In advance principal, penalty, fee, interest"/>
<column name="enum_type" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet id="2" author="fineract">
<insert tableName="ref_loan_transaction_processing_strategy">
<column name="id" valueNumeric="8"/>
<column name="code" value="due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest-strategy"/>
<column name="name" value="Due penalty, fee, interest, principal, In advance principal, penalty, fee, interest"/>
<column name="sort_order" valueNumeric="8"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,149 @@
/**
* 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 static org.junit.Assert.assertEquals;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
import org.junit.Test;
import org.springframework.test.util.ReflectionTestUtils;
public class LoanChargeEffectiveDueDateComparatorTest {
@Test
public void testLoanChargeEffectiveDueDateComparator() {
LoanCharge lc1 = new LoanCharge();
ReflectionTestUtils.setField(lc1, "chargeTime", ChargeTimeType.SPECIFIED_DUE_DATE.getValue());
ReflectionTestUtils.setField(lc1, "dueDate", LocalDate.of(2023, 3, 17));
LoanCharge lc2 = new LoanCharge();
ReflectionTestUtils.setField(lc2, "chargeTime", ChargeTimeType.SPECIFIED_DUE_DATE.getValue());
ReflectionTestUtils.setField(lc2, "dueDate", LocalDate.of(2023, 3, 18));
LoanCharge lc3 = new LoanCharge();
ReflectionTestUtils.setField(lc3, "chargeTime", ChargeTimeType.SPECIFIED_DUE_DATE.getValue());
ReflectionTestUtils.setField(lc3, "dueDate", LocalDate.of(2023, 3, 16));
LoanCharge lc4 = new LoanCharge();
LoanInstallmentCharge installmentCharge = new LoanInstallmentCharge();
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment();
ReflectionTestUtils.setField(installment, "dueDate", LocalDate.of(2023, 3, 15));
ReflectionTestUtils.setField(installmentCharge, "installment", installment);
ReflectionTestUtils.setField(lc4, "chargeTime", ChargeTimeType.INSTALMENT_FEE.getValue());
ReflectionTestUtils.setField(lc4, "loanInstallmentCharge", Set.of(installmentCharge));
List<LoanCharge> list = new ArrayList<>();
list.add(lc1);
list.add(lc2);
list.add(lc3);
list.add(lc4);
list.sort(LoanChargeEffectiveDueDateComparator.INSTANCE);
assertEquals(LocalDate.of(2023, 3, 15), list.get(0).getEffectiveDueDate());
assertEquals(LocalDate.of(2023, 3, 16), list.get(1).getEffectiveDueDate());
assertEquals(LocalDate.of(2023, 3, 17), list.get(2).getEffectiveDueDate());
assertEquals(LocalDate.of(2023, 3, 18), list.get(3).getEffectiveDueDate());
}
@Test
public void testBothNull() {
LoanCharge lc1 = new LoanCharge();
ReflectionTestUtils.setField(lc1, "chargeTime", ChargeTimeType.SPECIFIED_DUE_DATE.getValue());
ReflectionTestUtils.setField(lc1, "dueDate", null);
LoanCharge lc2 = new LoanCharge();
ReflectionTestUtils.setField(lc2, "chargeTime", ChargeTimeType.SPECIFIED_DUE_DATE.getValue());
ReflectionTestUtils.setField(lc2, "dueDate", null);
List<LoanCharge> list = new ArrayList<>();
list.add(lc1);
list.add(lc2);
list.sort(LoanChargeEffectiveDueDateComparator.INSTANCE);
assertEquals(lc1, list.get(0));
assertEquals(lc2, list.get(1));
list = new ArrayList<>();
list.add(lc2);
list.add(lc1);
list.sort(LoanChargeEffectiveDueDateComparator.INSTANCE);
assertEquals(lc2, list.get(0));
assertEquals(lc1, list.get(1));
}
@Test
public void testOneOfThemIsNull() {
LoanCharge lc1 = new LoanCharge();
ReflectionTestUtils.setField(lc1, "chargeTime", ChargeTimeType.SPECIFIED_DUE_DATE.getValue());
ReflectionTestUtils.setField(lc1, "dueDate", null);
LoanCharge lc2 = new LoanCharge();
ReflectionTestUtils.setField(lc2, "chargeTime", ChargeTimeType.SPECIFIED_DUE_DATE.getValue());
ReflectionTestUtils.setField(lc2, "dueDate", LocalDate.of(2023, 3, 17));
LoanCharge lc3 = new LoanCharge();
LoanInstallmentCharge installmentCharge1 = new LoanInstallmentCharge();
LoanRepaymentScheduleInstallment installment1 = new LoanRepaymentScheduleInstallment();
ReflectionTestUtils.setField(installment1, "dueDate", LocalDate.of(2023, 3, 15));
ReflectionTestUtils.setField(installmentCharge1, "installment", installment1);
ReflectionTestUtils.setField(lc3, "chargeTime", ChargeTimeType.INSTALMENT_FEE.getValue());
ReflectionTestUtils.setField(lc3, "loanInstallmentCharge", Set.of(installmentCharge1));
LoanCharge lc4 = new LoanCharge();
LoanInstallmentCharge installmentCharge2 = new LoanInstallmentCharge();
LoanRepaymentScheduleInstallment installment2 = new LoanRepaymentScheduleInstallment();
ReflectionTestUtils.setField(installment2, "dueDate", null);
ReflectionTestUtils.setField(installmentCharge2, "paid", Boolean.TRUE);
ReflectionTestUtils.setField(installmentCharge2, "installment", installment2);
ReflectionTestUtils.setField(lc4, "chargeTime", ChargeTimeType.INSTALMENT_FEE.getValue());
ReflectionTestUtils.setField(lc4, "loanInstallmentCharge", Set.of(installmentCharge2));
List<LoanCharge> list = new ArrayList<>();
list.add(lc1);
list.add(lc2);
list.add(lc3);
list.add(lc4);
list.sort(LoanChargeEffectiveDueDateComparator.INSTANCE);
assertEquals(lc3, list.get(0));
assertEquals(lc2, list.get(1));
assertEquals(lc1, list.get(2));
assertEquals(lc4, list.get(3));
// Reversed order
list = new ArrayList<>();
list.add(lc4);
list.add(lc3);
list.add(lc2);
list.add(lc1);
list.sort(LoanChargeEffectiveDueDateComparator.INSTANCE);
assertEquals(lc3, list.get(0));
assertEquals(lc2, list.get(1));
assertEquals(lc4, list.get(2));
assertEquals(lc1, list.get(3));
}
}
@@ -60,6 +60,7 @@ fineract.loan.transactionprocessor.heavensfamily.enabled=true
fineract.loan.transactionprocessor.interest-principal-penalties-fees.enabled=true
fineract.loan.transactionprocessor.principal-interest-penalties-fees.enabled=true
fineract.loan.transactionprocessor.rbi-india.enabled=true
fineract.loan.transactionprocessor.due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest.enabled=true
fineract.loan.transactionprocessor.error-not-found-fail=true
fineract.content.regex-whitelist-enabled=true
@@ -0,0 +1,428 @@
/**
* 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.integrationtests;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.util.HashMap;
import org.apache.fineract.client.models.BusinessDateRequest;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.accounting.Account;
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DueDateRespectiveLoanRepaymentScheduleTest {
private static final Logger LOG = LoggerFactory.getLogger(DueDateRespectiveLoanRepaymentScheduleTest.class);
private ResponseSpecification responseSpec;
private RequestSpecification requestSpec;
private BusinessDateHelper businessDateHelper;
private LoanTransactionHelper loanTransactionHelper;
private AccountHelper accountHelper;
@BeforeEach
public void setup() {
Utils.initializeRESTAssured();
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.requestSpec.header("Fineract-Platform-TenantId", "default");
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
this.businessDateHelper = new BusinessDateHelper();
this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec);
}
// Scenario1:
// 1. Disburse the loan
// 2. Adding a partial repayment
// 3. Adding a charge
// 3.1 No reverse-replay
// 4 Adding a partial repayment
// 4.1 Paying only principal portion
// 4.2 Adding a partial repayment
// 4.3 Paying only charge portion
@Test
public void scenario1() {
try {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
.date("2023.02.01").dateFormat("yyyy.MM.dd").locale("en"));
final Account assetAccount = this.accountHelper.createAssetAccount();
final Account incomeAccount = this.accountHelper.createIncomeAccount();
final Account expenseAccount = this.accountHelper.createExpenseAccount();
final Account overpaymentAccount = this.accountHelper.createLiabilityAccount();
Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", true));
final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "1", "0",
LoanProductTestBuilder.DUE_DATE_RESPECTIVE_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023");
final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "1", "1", "1", "0",
LoanApplicationTestBuilder.DUE_DATE_RESPECTIVE_STRATEGY, "01 January 2023", "01 January 2023");
HashMap<String, Object> loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID);
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
loanStatusHashMap = loanTransactionHelper.approveLoan("01 January 2023", loanID);
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
loanStatusHashMap = loanTransactionHelper.disburseLoanWithTransactionAmount("01 January 2023", loanID, "1000");
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
Integer firstRepaymentId = (Integer) loanTransactionHelper.makeRepayment("10 January 2023", Float.parseFloat("500.00"), loanID)
.get("resourceId");
Integer firstChargeId = loanTransactionHelper.addChargesForLoan(loanID,
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "20 January 2023", "50"));
Integer secondRepaymentId = (Integer) loanTransactionHelper.makeRepayment("17 January 2023", Float.parseFloat("450.00"), loanID)
.get("resourceId");
Integer thirdRepaymentId = (Integer) loanTransactionHelper.makeRepayment("21 January 2023", Float.parseFloat("50.00"), loanID)
.get("resourceId");
GetLoansLoanIdResponse response = loanTransactionHelper.getLoanDetails((long) loanID);
assertEquals(50.0, response.getSummary().getTotalOutstanding());
assertEquals(50.0, response.getRepaymentSchedule().getTotalOutstanding());
assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue());
assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid());
assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding());
assertEquals(1000.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue());
assertEquals(950.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid());
assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding());
assertTrue(response.getStatus().getActive());
assertEquals(firstRepaymentId, response.getTransactions().get(1).getId().intValue());
assertNull(response.getTransactions().get(1).getReversedOnDate());
assertTrue(response.getTransactions().get(1).getTransactionRelations().isEmpty());
assertTrue(response.getTransactions().get(1).getType().getRepayment());
assertEquals(500.0, response.getTransactions().get(1).getAmount());
assertEquals(500.0, response.getTransactions().get(1).getPrincipalPortion());
assertEquals(0.0, response.getTransactions().get(1).getPenaltyChargesPortion());
assertEquals(0.0, response.getTransactions().get(1).getOverpaymentPortion());
assertEquals(0.0, response.getTransactions().get(1).getInterestPortion());
assertEquals(0.0, response.getTransactions().get(1).getFeeChargesPortion());
assertEquals(500.0, response.getTransactions().get(1).getOutstandingLoanBalance());
assertEquals(secondRepaymentId, response.getTransactions().get(2).getId().intValue());
assertNull(response.getTransactions().get(2).getReversedOnDate());
assertTrue(response.getTransactions().get(2).getTransactionRelations().isEmpty());
assertTrue(response.getTransactions().get(2).getType().getRepayment());
assertEquals(450.0, response.getTransactions().get(2).getAmount());
assertEquals(450.0, response.getTransactions().get(2).getPrincipalPortion());
assertEquals(0.0, response.getTransactions().get(2).getPenaltyChargesPortion());
assertEquals(0.0, response.getTransactions().get(2).getOverpaymentPortion());
assertEquals(0.0, response.getTransactions().get(2).getInterestPortion());
assertEquals(0.0, response.getTransactions().get(2).getFeeChargesPortion());
assertEquals(50.0, response.getTransactions().get(2).getOutstandingLoanBalance());
assertEquals(thirdRepaymentId, response.getTransactions().get(3).getId().intValue());
assertNull(response.getTransactions().get(3).getReversedOnDate());
assertTrue(response.getTransactions().get(3).getTransactionRelations().isEmpty());
assertTrue(response.getTransactions().get(3).getType().getRepayment());
assertEquals(50.0, response.getTransactions().get(3).getAmount());
assertEquals(0.0, response.getTransactions().get(3).getPrincipalPortion());
assertEquals(50.0, response.getTransactions().get(3).getPenaltyChargesPortion());
assertEquals(0.0, response.getTransactions().get(3).getOverpaymentPortion());
assertEquals(0.0, response.getTransactions().get(3).getInterestPortion());
assertEquals(0.0, response.getTransactions().get(3).getFeeChargesPortion());
assertEquals(50.0, response.getTransactions().get(3).getOutstandingLoanBalance());
assertEquals(firstChargeId, response.getTransactions().get(3).getLoanChargePaidByList().get(0).getChargeId().intValue());
assertEquals(1, response.getTransactions().get(3).getLoanChargePaidByList().size());
} finally {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
}
}
// Scenario2:
// 1. Disburse the loan
// 2. Adding a partial repayment
// 3. Adding a charge
// 3.1 No reverse-replay
// 4. Adding a partial repayment
// 4.1 Paying only principal portion
// 5. Adding a charge
// 5.1 No any reverse-replay
// 6. Adding a partial repayment
// 6.1 Paying the 1st charge
// 6.2 Paying secondly the in advance principal
// 6.3 Not paying the 2nd charge (id: #5)
@Test
public void scenario2() {
try {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
.date("2023.02.01").dateFormat("yyyy.MM.dd").locale("en"));
final Account assetAccount = this.accountHelper.createAssetAccount();
final Account incomeAccount = this.accountHelper.createIncomeAccount();
final Account expenseAccount = this.accountHelper.createExpenseAccount();
final Account overpaymentAccount = this.accountHelper.createLiabilityAccount();
Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", true));
Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false));
final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "1", "0",
LoanProductTestBuilder.DUE_DATE_RESPECTIVE_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023");
final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "1", "1", "1", "0",
LoanApplicationTestBuilder.DUE_DATE_RESPECTIVE_STRATEGY, "01 January 2023", "01 January 2023");
HashMap<String, Object> loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID);
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
loanStatusHashMap = loanTransactionHelper.approveLoan("01 January 2023", loanID);
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
loanStatusHashMap = loanTransactionHelper.disburseLoanWithTransactionAmount("01 January 2023", loanID, "1000");
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
Integer firstRepaymentId = (Integer) loanTransactionHelper.makeRepayment("10 January 2023", Float.parseFloat("500.00"), loanID)
.get("resourceId");
Integer firstChargeId = loanTransactionHelper.addChargesForLoan(loanID,
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(fee), "20 January 2023", "50"));
Integer secondRepaymentId = (Integer) loanTransactionHelper.makeRepayment("17 January 2023", Float.parseFloat("100.00"), loanID)
.get("resourceId");
Integer secondChargeId = loanTransactionHelper.addChargesForLoan(loanID,
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "23 January 2023", "10"));
Integer thirdRepaymentId = (Integer) loanTransactionHelper.makeRepayment("20 January 2023", Float.parseFloat("100.00"), loanID)
.get("resourceId");
GetLoansLoanIdResponse response = loanTransactionHelper.getLoanDetails((long) loanID);
assertEquals(360.0, response.getSummary().getTotalOutstanding());
assertEquals(360.0, response.getRepaymentSchedule().getTotalOutstanding());
assertEquals(10.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue());
assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid());
assertEquals(10.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding());
assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue());
assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid());
assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding());
assertEquals(1000.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue());
assertEquals(650.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid());
assertEquals(350.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding());
assertTrue(response.getStatus().getActive());
assertEquals(firstRepaymentId, response.getTransactions().get(1).getId().intValue());
assertNull(response.getTransactions().get(1).getReversedOnDate());
assertTrue(response.getTransactions().get(1).getTransactionRelations().isEmpty());
assertTrue(response.getTransactions().get(1).getType().getRepayment());
assertEquals(500.0, response.getTransactions().get(1).getAmount());
assertEquals(500.0, response.getTransactions().get(1).getPrincipalPortion());
assertEquals(0.0, response.getTransactions().get(1).getPenaltyChargesPortion());
assertEquals(0.0, response.getTransactions().get(1).getOverpaymentPortion());
assertEquals(0.0, response.getTransactions().get(1).getInterestPortion());
assertEquals(0.0, response.getTransactions().get(1).getFeeChargesPortion());
assertEquals(500.0, response.getTransactions().get(1).getOutstandingLoanBalance());
assertEquals(secondRepaymentId, response.getTransactions().get(2).getId().intValue());
assertNull(response.getTransactions().get(2).getReversedOnDate());
assertTrue(response.getTransactions().get(2).getTransactionRelations().isEmpty());
assertTrue(response.getTransactions().get(2).getType().getRepayment());
assertEquals(100.0, response.getTransactions().get(2).getAmount());
assertEquals(100.0, response.getTransactions().get(2).getPrincipalPortion());
assertEquals(0.0, response.getTransactions().get(2).getPenaltyChargesPortion());
assertEquals(0.0, response.getTransactions().get(2).getOverpaymentPortion());
assertEquals(0.0, response.getTransactions().get(2).getInterestPortion());
assertEquals(0.0, response.getTransactions().get(2).getFeeChargesPortion());
assertEquals(400.0, response.getTransactions().get(2).getOutstandingLoanBalance());
assertEquals(thirdRepaymentId, response.getTransactions().get(3).getId().intValue());
assertNull(response.getTransactions().get(3).getReversedOnDate());
assertTrue(response.getTransactions().get(3).getTransactionRelations().isEmpty());
assertTrue(response.getTransactions().get(3).getType().getRepayment());
assertEquals(100.0, response.getTransactions().get(3).getAmount());
assertEquals(50.0, response.getTransactions().get(3).getPrincipalPortion());
assertEquals(50.0, response.getTransactions().get(3).getFeeChargesPortion());
assertEquals(0.0, response.getTransactions().get(3).getOverpaymentPortion());
assertEquals(0.0, response.getTransactions().get(3).getInterestPortion());
assertEquals(0.0, response.getTransactions().get(3).getPenaltyChargesPortion());
assertEquals(350.0, response.getTransactions().get(3).getOutstandingLoanBalance());
assertEquals(firstChargeId, response.getTransactions().get(3).getLoanChargePaidByList().get(0).getChargeId().intValue());
assertEquals(1, response.getTransactions().get(3).getLoanChargePaidByList().size());
} finally {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
}
}
// Scenario3:
// 1. Disburse the loan
// 2. Adding a partial repayment
// 3. Adding a charge
// 3.1 No reverse-replay
// 4. Adding a full repayment
// 4.1 Paying first the in advanced principal portion, and after the in advanced charges
@Test
public void scenario3() {
try {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
.date("2023.02.01").dateFormat("yyyy.MM.dd").locale("en"));
final Account assetAccount = this.accountHelper.createAssetAccount();
final Account incomeAccount = this.accountHelper.createIncomeAccount();
final Account expenseAccount = this.accountHelper.createExpenseAccount();
final Account overpaymentAccount = this.accountHelper.createLiabilityAccount();
Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false));
final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "1", "0",
LoanProductTestBuilder.DUE_DATE_RESPECTIVE_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023");
final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "1", "1", "1", "0",
LoanApplicationTestBuilder.DUE_DATE_RESPECTIVE_STRATEGY, "01 January 2023", "01 January 2023");
HashMap<String, Object> loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID);
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
loanStatusHashMap = loanTransactionHelper.approveLoan("01 January 2023", loanID);
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
loanStatusHashMap = loanTransactionHelper.disburseLoanWithTransactionAmount("01 January 2023", loanID, "1000");
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
Integer firstRepaymentId = (Integer) loanTransactionHelper.makeRepayment("10 January 2023", Float.parseFloat("500.00"), loanID)
.get("resourceId");
Integer firstChargeId = loanTransactionHelper.addChargesForLoan(loanID,
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(fee), "20 January 2023", "50"));
Integer secondRepaymentId = (Integer) loanTransactionHelper.makeRepayment("17 January 2023", Float.parseFloat("550.00"), loanID)
.get("resourceId");
GetLoansLoanIdResponse response = loanTransactionHelper.getLoanDetails((long) loanID);
assertEquals(0.0, response.getSummary().getTotalOutstanding());
assertEquals(0.0, response.getRepaymentSchedule().getTotalOutstanding());
assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue());
assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid());
assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding());
assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue());
assertEquals(50.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid());
assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding());
assertEquals(1000.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue());
assertEquals(1000.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid());
assertEquals(0.0, response.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding());
assertTrue(response.getStatus().getClosedObligationsMet());
assertEquals(firstRepaymentId, response.getTransactions().get(1).getId().intValue());
assertNull(response.getTransactions().get(1).getReversedOnDate());
assertTrue(response.getTransactions().get(1).getTransactionRelations().isEmpty());
assertTrue(response.getTransactions().get(1).getType().getRepayment());
assertEquals(500.0, response.getTransactions().get(1).getAmount());
assertEquals(500.0, response.getTransactions().get(1).getPrincipalPortion());
assertEquals(0.0, response.getTransactions().get(1).getPenaltyChargesPortion());
assertEquals(0.0, response.getTransactions().get(1).getOverpaymentPortion());
assertEquals(0.0, response.getTransactions().get(1).getInterestPortion());
assertEquals(0.0, response.getTransactions().get(1).getFeeChargesPortion());
assertEquals(500.0, response.getTransactions().get(1).getOutstandingLoanBalance());
int repaymentOrderNo;
int accrualOrderNo;
if (response.getTransactions().get(2).getType().getAccrual()) {
accrualOrderNo = 2;
repaymentOrderNo = 3;
} else {
accrualOrderNo = 3;
repaymentOrderNo = 2;
}
assertNull(response.getTransactions().get(accrualOrderNo).getReversedOnDate());
assertTrue(response.getTransactions().get(accrualOrderNo).getTransactionRelations().isEmpty());
assertTrue(response.getTransactions().get(accrualOrderNo).getType().getAccrual());
assertEquals(50.0, response.getTransactions().get(accrualOrderNo).getAmount());
assertEquals(0.0, response.getTransactions().get(accrualOrderNo).getPrincipalPortion());
assertEquals(0.0, response.getTransactions().get(accrualOrderNo).getPenaltyChargesPortion());
assertEquals(0.0, response.getTransactions().get(accrualOrderNo).getOverpaymentPortion());
assertEquals(0.0, response.getTransactions().get(accrualOrderNo).getInterestPortion());
assertEquals(50.0, response.getTransactions().get(accrualOrderNo).getFeeChargesPortion());
assertEquals(0.0, response.getTransactions().get(accrualOrderNo).getOutstandingLoanBalance());
assertEquals(firstChargeId,
response.getTransactions().get(accrualOrderNo).getLoanChargePaidByList().get(0).getChargeId().intValue());
assertEquals(1, response.getTransactions().get(accrualOrderNo).getLoanChargePaidByList().size());
assertEquals(secondRepaymentId, response.getTransactions().get(repaymentOrderNo).getId().intValue());
assertNull(response.getTransactions().get(repaymentOrderNo).getReversedOnDate());
assertTrue(response.getTransactions().get(repaymentOrderNo).getTransactionRelations().isEmpty());
assertTrue(response.getTransactions().get(repaymentOrderNo).getType().getRepayment());
assertEquals(550.0, response.getTransactions().get(repaymentOrderNo).getAmount());
assertEquals(500.0, response.getTransactions().get(repaymentOrderNo).getPrincipalPortion());
assertEquals(0.0, response.getTransactions().get(repaymentOrderNo).getPenaltyChargesPortion());
assertEquals(0.0, response.getTransactions().get(repaymentOrderNo).getOverpaymentPortion());
assertEquals(0.0, response.getTransactions().get(repaymentOrderNo).getInterestPortion());
assertEquals(50.0, response.getTransactions().get(repaymentOrderNo).getFeeChargesPortion());
assertEquals(0.0, response.getTransactions().get(repaymentOrderNo).getOutstandingLoanBalance());
assertEquals(firstChargeId,
response.getTransactions().get(repaymentOrderNo).getLoanChargePaidByList().get(0).getChargeId().intValue());
assertEquals(1, response.getTransactions().get(repaymentOrderNo).getLoanChargePaidByList().size());
} finally {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
}
}
private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, final String principal,
final String loanTermFrequency, final String repaymentAfterEvery, final String numberOfRepayments, final String interestRate,
final String repaymentStrategy, final String expectedDisbursementDate, final String submittedOnDate) {
LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principal)
.withLoanTermFrequency(loanTermFrequency).withLoanTermFrequencyAsMonths().withNumberOfRepayments(numberOfRepayments)
.withRepaymentEveryAfter(repaymentAfterEvery).withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod(interestRate)
.withInterestTypeAsFlatBalance().withAmortizationTypeAsEqualPrincipalPayments().withRepaymentStrategy(repaymentStrategy)
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate(expectedDisbursementDate)
.withSubmittedOnDate(submittedOnDate).withLoanType("individual").build(clientID.toString(), loanProductID.toString(), null);
return loanTransactionHelper.getLoanId(loanApplicationJSON);
}
private Integer createLoanProductWithNoAccountingNoInterest(final String principal, final String repaymentAfterEvery,
final String numberOfRepayments, final String interestRate, final String repaymentStrategy, final Account... accounts) {
LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
final String loanProductJSON = new LoanProductTestBuilder().withPrincipal(principal).withRepaymentTypeAsMonth()
.withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments).withRepaymentTypeAsMonth()
.withinterestRatePerPeriod(interestRate).withInterestRateFrequencyTypeAsMonths().withRepaymentStrategy(repaymentStrategy)
.withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts)
.withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0").build(null);
return loanTransactionHelper.getLoanProductId(loanProductJSON);
}
}
@@ -42,6 +42,7 @@ public class LoanApplicationTestBuilder {
public static final String DEFAULT_STRATEGY = "mifos-standard-strategy";
public static final String RBI_INDIA_STRATEGY = "rbi-india-strategy";
public static final String INTEREST_PRINCIPAL_PENALTIES_FEES_ORDER_STRATEGY = "interest-principal-penalties-fees-order-strategy";
public static final String DUE_DATE_RESPECTIVE_STRATEGY = "due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest-strategy";
private String externalId = null;
private String principal = "10,000";
@@ -42,6 +42,7 @@ public class LoanProductTestBuilder {
private static final String FLAT_BALANCE = "1";
public static final String DEFAULT_STRATEGY = "mifos-standard-strategy";
public static final String INTEREST_PRINCIPAL_PENALTIES_FEES_ORDER_STRATEGY = "interest-principal-penalties-fees-order-strategy";
public static final String DUE_DATE_RESPECTIVE_STRATEGY = "due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest-strategy";
// private static final String HEAVENS_FAMILY_STRATEGY ="heavensfamily-strategy";
// private static final String CREO_CORE_STRATEGY ="creocore-strategy";
public static final String RBI_INDIA_STRATEGY = "rbi-india-strategy";