FINERACT-1968: Support horizontal and vertical installment processing

This commit is contained in:
Adam Saghy
2023-11-13 11:56:02 +01:00
parent 3799835414
commit 1eab93c8f3
42 changed files with 1270 additions and 278 deletions
@@ -24,9 +24,9 @@ import org.springframework.stereotype.Component;
@Component @Component
public class AcmeLoanRepaymentScheduleTransactionProcessor extends FineractStyleLoanRepaymentScheduleTransactionProcessor { public class AcmeLoanRepaymentScheduleTransactionProcessor extends FineractStyleLoanRepaymentScheduleTransactionProcessor {
private static final String STRATEGY_CODE = "acme-standard-strategy"; public static final String STRATEGY_CODE = "acme-standard-strategy";
private static final String STRATEGY_NAME = "ACME Corp.: standard loan transaction processing strategy"; public static final String STRATEGY_NAME = "ACME Corp.: standard loan transaction processing strategy";
@Override @Override
public String getCode() { public String getCode() {
@@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
@Entity @Entity
@@ -424,6 +425,24 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
this.penaltyCharges = null; this.penaltyCharges = null;
} }
public interface PaymentFunction {
Money accept(LocalDate transactionDate, Money transactionAmountRemaining);
}
public PaymentFunction getPaymentFunction(AllocationType allocationType, PaymentAction action) {
return switch (allocationType) {
case PENALTY -> PaymentAction.PAY.equals(action) ? this::payPenaltyChargesComponent
: PaymentAction.UNPAY.equals(action) ? this::unpayPenaltyChargesComponent : null;
case FEE -> PaymentAction.PAY.equals(action) ? this::payFeeChargesComponent
: PaymentAction.UNPAY.equals(action) ? this::unpayFeeChargesComponent : null;
case INTEREST -> PaymentAction.PAY.equals(action) ? this::payInterestComponent
: PaymentAction.UNPAY.equals(action) ? this::unpayInterestComponent : null;
case PRINCIPAL -> PaymentAction.PAY.equals(action) ? this::payPrincipalComponent
: PaymentAction.UNPAY.equals(action) ? this::unpayPrincipalComponent : null;
};
}
public Money payPenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { public Money payPenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
@@ -967,4 +986,8 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
public void resetPrincipalDue() { public void resetPrincipalDue() {
this.principal = null; this.principal = null;
} }
public enum PaymentAction {
PAY, UNPAY
}
} }
@@ -765,7 +765,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
} }
private void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges, protected void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges,
final Integer installmentNumber) { final Integer installmentNumber) {
Money amountRemaining = feeCharges; Money amountRemaining = feeCharges;
@@ -18,15 +18,21 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl; package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.MathContext; import java.math.MathContext;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@@ -47,6 +53,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanproduct.domain.DueType;
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
@@ -58,7 +66,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy"; public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
private final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
@Override @Override
public String getCode() { public String getCode() {
@@ -104,6 +112,113 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
throw new NotImplementedException(); throw new NotImplementedException();
} }
@Override
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
if (charges != null) {
for (final LoanCharge loanCharge : charges) {
if (!loanCharge.isDueAtDisbursement()) {
loanCharge.resetPaidAmount(currency);
}
}
}
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
currentInstallment.resetBalances();
currentInstallment.updateDerivedFields(currency, disbursementDate);
}
List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges);
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency,
installments, charges, changedTransactionDetail));
chargeOrTransaction.getLoanCharge()
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
}
reprocessInstallments(installments, currency);
return changedTransactionDetail;
}
@Override
public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, Money overpaidAmount) {
switch (loanTransaction.getTypeOf()) {
case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments);
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments);
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
handleRepayment(loanTransaction, currency, installments, charges);
case CHARGE_OFF -> handleChargeOff(loanTransaction, currency, installments);
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, currency, installments, charges);
// TODO: Cover rest of the transaction types
default -> {
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
}
}
}
@Override
protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
Money zero = Money.zero(currency);
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
.orElse(defaultPaymentAllocationRule);
Balances balances = new Balances(zero, zero, zero, zero);
List<PaymentAllocationType> paymentAllocationTypes;
FutureInstallmentAllocationRule futureInstallmentAllocationRule;
if (PaymentAllocationTransactionType.DEFAULT.equals(paymentAllocationRule.getTransactionType())) {
// if the allocation rule is not defined then the reverse order of the default allocation rule will be used
paymentAllocationTypes = new ArrayList<>(paymentAllocationRule.getAllocationTypes());
Collections.reverse(paymentAllocationTypes);
futureInstallmentAllocationRule = FutureInstallmentAllocationRule.LAST_INSTALLMENT;
} else {
paymentAllocationTypes = paymentAllocationRule.getAllocationTypes();
futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
}
if (LoanScheduleProcessingType.HORIZONTAL
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
// TODO: implementation
} else if (LoanScheduleProcessingType.VERTICAL
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
transactionAmountUnprocessed = refundTransaction(loanTransaction, currency, installments, zero, transactionMappings,
transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, paymentAllocationType);
if (!transactionAmountUnprocessed.isGreaterThanZero()) {
break;
}
}
}
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
Integer installmentNumber = null;
final Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
if (feeCharges.isGreaterThanZero()) {
undoChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, installmentNumber);
}
final Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
if (penaltyCharges.isGreaterThanZero()) {
undoChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, installmentNumber);
}
}
private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges,
ChangedTransactionDetail changedTransactionDetail) { ChangedTransactionDetail changedTransactionDetail) {
@@ -139,37 +254,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
loanChargeProcessor.reprocess(currency, disbursementDate, installments, loanCharge); loanChargeProcessor.reprocess(currency, disbursementDate, installments, loanCharge);
} }
@Override
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
// TODO: rewrite this whole logic step by step
if (charges != null) {
for (final LoanCharge loanCharge : charges) {
if (!loanCharge.isDueAtDisbursement()) {
loanCharge.resetPaidAmount(currency);
}
}
}
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
currentInstallment.resetBalances();
currentInstallment.updateDerivedFields(currency, disbursementDate);
}
List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges);
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency,
installments, charges, changedTransactionDetail));
chargeOrTransaction.getLoanCharge()
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
}
reprocessInstallments(installments, currency);
return changedTransactionDetail;
}
@NotNull @NotNull
private List<ChargeOrTransaction> createSortedChargesAndTransactionsList(List<LoanTransaction> loanTransactions, private List<ChargeOrTransaction> createSortedChargesAndTransactionsList(List<LoanTransaction> loanTransactions,
Set<LoanCharge> charges) { Set<LoanCharge> charges) {
@@ -184,26 +268,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return chargeOrTransactions; return chargeOrTransactions;
} }
@Override
public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, Money overpaidAmount) {
switch (loanTransaction.getTypeOf()) {
case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments);
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments);
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
handleRepayment(loanTransaction, currency, installments, charges);
case CHARGE_OFF -> handleChargeOff(loanTransaction, currency, installments);
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, currency, installments, charges);
// TODO: Cover rest of the transaction types
default -> {
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
}
}
}
private void handleDisbursement(LoanTransaction loanTransaction, MonetaryCurrency currency, private void handleDisbursement(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments) { List<LoanRepaymentScheduleInstallment> installments) {
updateLoanSchedule(loanTransaction, currency, installments); updateLoanSchedule(loanTransaction, currency, installments);
@@ -271,68 +335,44 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return loanTransactionToRepaymentScheduleMapping; return loanTransactionToRepaymentScheduleMapping;
} }
private Money payAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment, private Money processPaymentAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment,
LoanTransaction loanTransaction, Money transactionAmountUnprocessed, LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances) { LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances,
LoanRepaymentScheduleInstallment.PaymentAction action) {
LocalDate transactionDate = loanTransaction.getTransactionDate(); LocalDate transactionDate = loanTransaction.getTransactionDate();
Money zero = transactionAmountUnprocessed.zero(); Money zero = transactionAmountUnprocessed.zero();
return switch (paymentAllocationType.getAllocationType()) { return switch (paymentAllocationType.getAllocationType()) {
case PENALTY -> { case PENALTY -> {
Money subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountUnprocessed); LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(subPenaltyPortion)); .getPaymentFunction(paymentAllocationType.getAllocationType(), action);
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, subPenaltyPortion); Money portion = paymentFunction.accept(transactionDate, transactionAmountUnprocessed);
yield subPenaltyPortion; balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(portion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, portion);
yield portion;
} }
case FEE -> { case FEE -> {
Money subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountUnprocessed); LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(subFeePortion)); .getPaymentFunction(paymentAllocationType.getAllocationType(), action);
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, subFeePortion, zero); Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
yield subFeePortion; balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(portion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, portion, zero);
yield portion;
} }
case INTEREST -> { case INTEREST -> {
Money subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountUnprocessed); LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(subInterestPortion)); .getPaymentFunction(paymentAllocationType.getAllocationType(), action);
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, subInterestPortion, zero, zero); Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
yield subInterestPortion; balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(portion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, portion, zero, zero);
yield portion;
} }
case PRINCIPAL -> { case PRINCIPAL -> {
Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountUnprocessed); LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(subPrincipalPortion)); .getPaymentFunction(paymentAllocationType.getAllocationType(), action);
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, subPrincipalPortion, zero, zero, zero); Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
yield subPrincipalPortion; balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(portion));
} addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, portion, zero, zero, zero);
}; yield portion;
}
private Money unpayAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment,
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances) {
LocalDate transactionDate = loanTransaction.getTransactionDate();
Money zero = transactionAmountUnprocessed.zero();
return switch (paymentAllocationType.getAllocationType()) {
case PENALTY -> {
Money unpaidPenaltyPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().minus(unpaidPenaltyPortion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, unpaidPenaltyPortion);
yield unpaidPenaltyPortion;
}
case FEE -> {
Money unpaidFeePortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().minus(unpaidFeePortion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, unpaidFeePortion, zero);
yield unpaidFeePortion;
}
case INTEREST -> {
Money unpaidInterestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().minus(unpaidInterestPortion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, unpaidInterestPortion, zero, zero);
yield unpaidInterestPortion;
}
case PRINCIPAL -> {
Money unpaidPrincipalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().minus(unpaidPrincipalPortion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, unpaidPrincipalPortion, zero, zero, zero);
yield unpaidPrincipalPortion;
} }
}; };
} }
@@ -431,49 +471,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
} }
@Override
protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
Money zero = Money.zero(currency);
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
Optional<LoanPaymentAllocationRule> paymentAllocationRuleOptional = paymentAllocationRules.stream()
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst();
Balances balances = new Balances(zero, zero, zero, zero);
if (paymentAllocationRuleOptional.isPresent()) {
LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRuleOptional.get();
List<PaymentAllocationType> paymentAllocationTypes = paymentAllocationRule.getAllocationTypes();
FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
transactionAmountUnprocessed = refundTransaction(loanTransaction, currency, installments, zero, transactionMappings,
transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, paymentAllocationType);
if (!transactionAmountUnprocessed.isGreaterThanZero()) {
break;
}
}
} else {
// if the allocation rule is not defined then the reverse order of the default allocation rule will be used
LoanPaymentAllocationRule paymentAllocationRule = defaultPaymentAllocationRule;
List<PaymentAllocationType> paymentAllocationTypes = paymentAllocationRule.getAllocationTypes();
FutureInstallmentAllocationRule futureInstallmentAllocationRule = FutureInstallmentAllocationRule.LAST_INSTALLMENT;
for (int i = paymentAllocationTypes.size() - 1; i >= 0; i--) {
PaymentAllocationType paymentAllocationType = paymentAllocationTypes.get(i);
transactionAmountUnprocessed = refundTransaction(loanTransaction, currency, installments, zero, transactionMappings,
transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, paymentAllocationType);
if (!transactionAmountUnprocessed.isGreaterThanZero()) {
break;
}
}
}
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
}
private Money refundTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, private Money refundTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money zero, List<LoanRepaymentScheduleInstallment> installments, Money zero,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Money transactionAmountUnprocessed,
@@ -490,8 +487,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (currentInstallment != null) { if (currentInstallment != null) {
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
refundedPortion = unpayAllocation(paymentAllocationType, currentInstallment, loanTransaction, refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances); transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
} }
} }
@@ -502,8 +500,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (currentInstallment != null) { if (currentInstallment != null) {
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
refundedPortion = unpayAllocation(paymentAllocationType, currentInstallment, loanTransaction, refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances); transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
} }
} }
@@ -536,8 +535,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
Money internalUnpaidPortion = unpayAllocation(paymentAllocationType, currentInstallment, loanTransaction, Money internalUnpaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment,
evenPortion, loanTransactionToRepaymentScheduleMapping, balances); loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, balances,
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
if (internalUnpaidPortion.isGreaterThanZero()) { if (internalUnpaidPortion.isGreaterThanZero()) {
refundedPortion = internalUnpaidPortion; refundedPortion = internalUnpaidPortion;
} }
@@ -564,33 +564,163 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst() .filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
.orElse(defaultPaymentAllocationRule); .orElse(defaultPaymentAllocationRule);
Balances balances = new Balances(zero, zero, zero, zero); Balances balances = new Balances(zero, zero, zero, zero);
if (LoanScheduleProcessingType.HORIZONTAL
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, currency, installments, transactionAmountUnprocessed,
paymentAllocationRule, transactionMappings, balances);
} else if (LoanScheduleProcessingType.VERTICAL
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, currency, installments, transactionAmountUnprocessed,
paymentAllocationRule, transactionMappings, balances);
}
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
payAdditionalCharges(loanTransaction, currency, charges);
handleOverpayment(transactionAmountUnprocessed, loanTransaction);
}
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Balances balances) {
LinkedHashMap<DueType, List<PaymentAllocationType>> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream()
.collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new,
mapping(Function.identity(), toList())));
for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, currency, installments,
transactionAmountUnprocessed, paymentAllocationsEntry.getValue(),
paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, balances);
}
return transactionAmountUnprocessed;
}
private Money processAllocationsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances) {
Money paidPortion;
boolean exit = false;
do {
LoanRepaymentScheduleInstallment oldestPastDueInstallment = installments.stream()
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isAfter(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
LoanRepaymentScheduleInstallment dueInstallment = installments.stream()
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isOn(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
// For having similar logic we are populating installment list even when the future installment
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new ArrayList<>();
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate())).toList();
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
}
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
switch (paymentAllocationType.getDueType()) {
case PAST_DUE -> {
if (oldestPastDueInstallment != null) {
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, oldestPastDueInstallment, currency);
paidPortion = processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
} else {
exit = true;
}
}
case DUE -> {
if (dueInstallment != null) {
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, dueInstallment, currency);
paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
} else {
exit = true;
}
}
case IN_ADVANCE -> {
int numberOfInstallments = inAdvanceInstallments.size();
if (numberOfInstallments > 0) {
// This will be the same amount as transactionAmountUnprocessed in case of the future
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
// Adjustment might be needed due to the divide operation and the rounding mode
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
// Adjust the portion for the last installment
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
evenPortion = evenPortion.add(balanceAdjustment);
}
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, inAdvanceInstallment, currency);
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
evenPortion, loanTransactionToRepaymentScheduleMapping, balances,
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
}
} else {
exit = true;
}
}
}
}
}
// We are allocating till there is no pending installment or there is no more unprocessed transaction amount
// or there is no more outstanding balance of the allocation type
while (!exit && installments.stream().anyMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
&& transactionAmountUnprocessed.isGreaterThanZero());
return transactionAmountUnprocessed;
}
private Money processPeriodsVertically(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Balances balances) {
for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) { for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) {
FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule(); FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
LoanRepaymentScheduleInstallment currentInstallment = null; LoanRepaymentScheduleInstallment currentInstallment = null;
Money paidPortion = zero; Money paidPortion = Money.zero(currency);
do { do {
Predicate<LoanRepaymentScheduleInstallment> predicate = getFilterPredicate(paymentAllocationType, currency);
switch (paymentAllocationType.getDueType()) { switch (paymentAllocationType.getDueType()) {
case PAST_DUE -> { case PAST_DUE -> {
currentInstallment = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isAfter(e.getDueDate()))
.filter(e -> loanTransaction.isAfter(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
if (currentInstallment != null) { if (currentInstallment != null) {
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
paidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction, paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances); transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
} }
} }
case DUE -> { case DUE -> {
currentInstallment = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isOn(e.getDueDate()))
.filter(e -> loanTransaction.isOn(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
if (currentInstallment != null) { if (currentInstallment != null) {
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
paidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction, paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances); transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
} }
} }
@@ -599,19 +729,19 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element. // allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
List<LoanRepaymentScheduleInstallment> currentInstallments = new ArrayList<>(); List<LoanRepaymentScheduleInstallment> currentInstallments = new ArrayList<>();
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) { if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) currentInstallments = installments.stream().filter(predicate)
.filter(e -> loanTransaction.isBefore(e.getDueDate())).toList(); .filter(e -> loanTransaction.isBefore(e.getDueDate())).toList();
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) { } else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) currentInstallments = installments.stream().filter(predicate)
.filter(e -> loanTransaction.isBefore(e.getDueDate())) .filter(e -> loanTransaction.isBefore(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) { } else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) currentInstallments = installments.stream().filter(predicate)
.filter(e -> loanTransaction.isBefore(e.getDueDate())) .filter(e -> loanTransaction.isBefore(e.getDueDate()))
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
} }
int numberOfInstallments = currentInstallments.size(); int numberOfInstallments = currentInstallments.size();
paidPortion = zero; paidPortion = Money.zero(currency);
if (numberOfInstallments > 0) { if (numberOfInstallments > 0) {
// This will be the same amount as transactionAmountUnprocessed in case of the future // This will be the same amount as transactionAmountUnprocessed in case of the future
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT // installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
@@ -626,8 +756,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency); transactionMappings, loanTransaction, currentInstallment, currency);
Money internalPaidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction, Money internalPaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment,
evenPortion, loanTransactionToRepaymentScheduleMapping, balances); loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, balances,
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
// Some extra logic to allocate as much as possible across the installments if the // Some extra logic to allocate as much as possible across the installments if the
// outstanding balances are different // outstanding balances are different
if (internalPaidPortion.isGreaterThanZero()) { if (internalPaidPortion.isGreaterThanZero()) {
@@ -644,15 +775,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
// We are allocating till there is no pending installment or there is no more unprocessed transaction amount // We are allocating till there is no pending installment or there is no more unprocessed transaction amount
// or there is no more outstanding balance of the allocation type // or there is no more outstanding balance of the allocation type
while (currentInstallment != null && transactionAmountUnprocessed.isGreaterThanZero() && paidPortion.isGreaterThanZero()); while (currentInstallment != null && transactionAmountUnprocessed.isGreaterThanZero() && paidPortion.isGreaterThanZero());
} }
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(), return transactionAmountUnprocessed;
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion()); }
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
payAdditionalCharges(loanTransaction, currency, charges); private Predicate<LoanRepaymentScheduleInstallment> getFilterPredicate(PaymentAllocationType paymentAllocationType,
MonetaryCurrency currency) {
handleOverpayment(transactionAmountUnprocessed, loanTransaction); return switch (paymentAllocationType.getAllocationType()) {
case PENALTY -> (p) -> p.getPenaltyChargesOutstanding(currency).isGreaterThanZero();
case FEE -> (p) -> p.getFeeChargesOutstanding(currency).isGreaterThanZero();
case INTEREST -> (p) -> p.getInterestOutstanding(currency).isGreaterThanZero();
case PRINCIPAL -> (p) -> p.getPrincipalOutstanding(currency).isGreaterThanZero();
};
} }
private void payAdditionalCharges(LoanTransaction loanTransaction, MonetaryCurrency currency, Set<LoanCharge> charges) { private void payAdditionalCharges(LoanTransaction loanTransaction, MonetaryCurrency currency, Set<LoanCharge> charges) {
@@ -42,9 +42,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
*/ */
public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
private static final String STRATEGY_CODE = "creocore-strategy"; public static final String STRATEGY_CODE = "creocore-strategy";
private static final String STRATEGY_NAME = "Creocore Unique"; public static final String STRATEGY_NAME = "Creocore Unique";
@Override @Override
public String getCode() { public String getCode() {
@@ -43,9 +43,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor
extends AbstractLoanRepaymentScheduleTransactionProcessor { extends AbstractLoanRepaymentScheduleTransactionProcessor {
private static final String STRATEGY_CODE = "due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest-strategy"; public 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"; public static final String STRATEGY_NAME = "Due penalty, fee, interest, principal, In advance principal, penalty, fee, interest";
@Override @Override
public String getCode() { public String getCode() {
@@ -43,9 +43,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor
extends AbstractLoanRepaymentScheduleTransactionProcessor { extends AbstractLoanRepaymentScheduleTransactionProcessor {
private static final String STRATEGY_CODE = "due-penalty-interest-principal-fee-in-advance-penalty-interest-principal-fee-strategy"; public static final String STRATEGY_CODE = "due-penalty-interest-principal-fee-in-advance-penalty-interest-principal-fee-strategy";
private static final String STRATEGY_NAME = "Due penalty, interest, principal, fee, In advance penalty, interest, principal, fee"; public static final String STRATEGY_NAME = "Due penalty, interest, principal, fee, In advance penalty, interest, principal, fee";
@Override @Override
public String getCode() { public String getCode() {
@@ -38,7 +38,7 @@ public class EarlyPaymentLoanRepaymentScheduleTransactionProcessor extends Abstr
public static final String STRATEGY_CODE = "early-repayment-strategy"; public static final String STRATEGY_CODE = "early-repayment-strategy";
private static final String STRATEGY_NAME = "Early Repayment Strategy"; public static final String STRATEGY_NAME = "Early Repayment Strategy";
@Override @Override
public String getCode() { public String getCode() {
@@ -42,9 +42,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class FineractStyleLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { public class FineractStyleLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
private static final String STRATEGY_CODE = "mifos-standard-strategy"; public static final String STRATEGY_CODE = "mifos-standard-strategy";
private static final String STRATEGY_NAME = "Penalties, Fees, Interest, Principal order"; public static final String STRATEGY_NAME = "Penalties, Fees, Interest, Principal order";
@Override @Override
public String getCode() { public String getCode() {
@@ -44,9 +44,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
private static final String STRATEGY_CODE = "heavensfamily-strategy"; public static final String STRATEGY_CODE = "heavensfamily-strategy";
private static final String STRATEGY_NAME = "HeavensFamily Unique"; public static final String STRATEGY_NAME = "HeavensFamily Unique";
@Override @Override
public String getCode() { public String getCode() {
@@ -37,9 +37,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor
extends AbstractLoanRepaymentScheduleTransactionProcessor { extends AbstractLoanRepaymentScheduleTransactionProcessor {
private static final String STRATEGY_CODE = "interest-principal-penalties-fees-order-strategy"; public static final String STRATEGY_CODE = "interest-principal-penalties-fees-order-strategy";
private static final String STRATEGY_NAME = "Interest, Principal, Penalties, Fees Order"; public static final String STRATEGY_NAME = "Interest, Principal, Penalties, Fees Order";
@Override @Override
public String getCode() { public String getCode() {
@@ -37,9 +37,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor
extends AbstractLoanRepaymentScheduleTransactionProcessor { extends AbstractLoanRepaymentScheduleTransactionProcessor {
private static final String STRATEGY_CODE = "principal-interest-penalties-fees-order-strategy"; public static final String STRATEGY_CODE = "principal-interest-penalties-fees-order-strategy";
private static final String STRATEGY_NAME = "Principal, Interest, Penalties, Fees Order"; public static final String STRATEGY_NAME = "Principal, Interest, Penalties, Fees Order";
@Override @Override
public String getCode() { public String getCode() {
@@ -46,9 +46,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
*/ */
public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
private static final String STRATEGY_CODE = "rbi-india-strategy"; public static final String STRATEGY_CODE = "rbi-india-strategy";
private static final String STRATEGY_NAME = "Overdue/Due Fee/Int,Principal"; public static final String STRATEGY_NAME = "Overdue/Due Fee/Int,Principal";
@Override @Override
public String getCode() { public String getCode() {
@@ -219,6 +219,7 @@ public final class LoanApplicationTerms {
private boolean isScheduleExtensionForDownPaymentDisabled; private boolean isScheduleExtensionForDownPaymentDisabled;
private Money disbursedPrincipal; private Money disbursedPrincipal;
private final LoanScheduleType loanScheduleType; private final LoanScheduleType loanScheduleType;
private final LoanScheduleProcessingType loanScheduleProcessingType;
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
@@ -246,7 +247,7 @@ public final class LoanApplicationTerms {
final Boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final Boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
final Boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType, final Boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType,
final LocalDate submittedOnDate, final Boolean isScheduleExtensionForDownPaymentDisabled, final LocalDate submittedOnDate, final Boolean isScheduleExtensionForDownPaymentDisabled,
final LoanScheduleType loanScheduleType) { final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null; final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null; final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
@@ -265,7 +266,7 @@ public final class LoanApplicationTerms {
isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment, isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment,
isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled,
loanScheduleType); loanScheduleType, loanScheduleProcessingType);
} }
@@ -322,6 +323,7 @@ public final class LoanApplicationTerms {
isScheduleExtensionForDownPaymentDisabled = loanProductRelatedDetail.isDisableScheduleExtensionForDownPayment(); isScheduleExtensionForDownPaymentDisabled = loanProductRelatedDetail.isDisableScheduleExtensionForDownPayment();
} }
LoanScheduleType loanScheduleType = loanProductRelatedDetail.getLoanScheduleType(); LoanScheduleType loanScheduleType = loanProductRelatedDetail.getLoanScheduleType();
LoanScheduleProcessingType loanScheduleProcessingType = loanProductRelatedDetail.getLoanScheduleProcessingType();
return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
repaymentEvery, repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod, repaymentEvery, repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod,
interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate,
@@ -338,7 +340,7 @@ public final class LoanApplicationTerms {
isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment, isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment,
isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled,
loanScheduleType); loanScheduleType, loanScheduleProcessingType);
} }
private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
@@ -367,7 +369,8 @@ public final class LoanApplicationTerms {
final boolean isPrincipalCompoundingDisabledForOverdueLoans, final boolean isDownPaymentEnabled, final boolean isPrincipalCompoundingDisabledForOverdueLoans, final boolean isDownPaymentEnabled,
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean isAutoRepaymentForDownPaymentEnabled, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean isAutoRepaymentForDownPaymentEnabled,
final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate,
final boolean isScheduleExtensionForDownPaymentDisabled, final LoanScheduleType loanScheduleType) { final boolean isScheduleExtensionForDownPaymentDisabled, final LoanScheduleType loanScheduleType,
final LoanScheduleProcessingType loanScheduleProcessingType) {
this.currency = currency; this.currency = currency;
this.loanTermFrequency = loanTermFrequency; this.loanTermFrequency = loanTermFrequency;
@@ -452,6 +455,7 @@ public final class LoanApplicationTerms {
this.submittedOnDate = submittedOnDate; this.submittedOnDate = submittedOnDate;
this.isScheduleExtensionForDownPaymentDisabled = isScheduleExtensionForDownPaymentDisabled; this.isScheduleExtensionForDownPaymentDisabled = isScheduleExtensionForDownPaymentDisabled;
this.loanScheduleType = loanScheduleType; this.loanScheduleType = loanScheduleType;
this.loanScheduleProcessingType = loanScheduleProcessingType;
} }
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
@@ -1295,7 +1299,7 @@ public final class LoanApplicationTerms {
this.graceOnArrearsAgeing, this.daysInMonthType.getValue(), this.daysInYearType.getValue(), this.graceOnArrearsAgeing, this.daysInMonthType.getValue(), this.daysInYearType.getValue(),
this.interestRecalculationEnabled, this.isEqualAmortization, this.isDownPaymentEnabled, this.interestRecalculationEnabled, this.isEqualAmortization, this.isDownPaymentEnabled,
this.disbursedAmountPercentageForDownPayment, this.isAutoRepaymentForDownPaymentEnabled, this.disbursedAmountPercentageForDownPayment, this.isAutoRepaymentForDownPaymentEnabled,
this.isScheduleExtensionForDownPaymentDisabled, this.loanScheduleType); this.isScheduleExtensionForDownPaymentDisabled, this.loanScheduleType, this.loanScheduleProcessingType);
} }
public Integer getLoanTermFrequency() { public Integer getLoanTermFrequency() {
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import java.util.Arrays;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
@Getter
@RequiredArgsConstructor
public enum LoanScheduleProcessingType {
HORIZONTAL("Horizontal"), VERTICAL("Vertical");
private final String humanReadableName;
public static List<EnumOptionData> getValuesAsEnumOptionDataList() {
return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList();
}
public EnumOptionData asEnumOptionData() {
return new EnumOptionData((long) this.ordinal(), this.name(), this.humanReadableName);
}
}
@@ -18,6 +18,25 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import java.util.Arrays;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
@Getter
@RequiredArgsConstructor
public enum LoanScheduleType { public enum LoanScheduleType {
CUMULATIVE, PROGRESSIVE
CUMULATIVE("Cumulative"), PROGRESSIVE("Progressive");
private final String humanReadableName;
public static List<EnumOptionData> getValuesAsEnumOptionDataList() {
return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList();
}
public EnumOptionData asEnumOptionData() {
return new EnumOptionData((long) this.ordinal(), this.name(), this.humanReadableName);
}
} }
@@ -158,5 +158,6 @@ public interface LoanProductConstants {
// loan schedule type // loan schedule type
String LOAN_SCHEDULE_TYPE = "loanScheduleType"; String LOAN_SCHEDULE_TYPE = "loanScheduleType";
String LOAN_SCHEDULE_PROCESSING_TYPE = "loanScheduleProcessingType";
} }
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanproduct.domain;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
@@ -72,6 +73,34 @@ public class AdvancedPaymentAllocationsValidator {
} }
} }
public void checkGroupingOfAllocationRules(List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules) {
loanProductPaymentAllocationRules.forEach(paymentAllocationRule -> {
AtomicInteger pastDueRuleInteger = new AtomicInteger();
AtomicInteger dueRuleInteger = new AtomicInteger();
AtomicInteger inAdvanceRuleInteger = new AtomicInteger();
paymentAllocationRule.getAllocationTypes().forEach(paymentAllocationType -> {
validateAllocationType(paymentAllocationType, pastDueRuleInteger, dueRuleInteger, inAdvanceRuleInteger);
});
});
}
private void validateAllocationType(PaymentAllocationType paymentAllocationType, AtomicInteger pastDueRuleInteger,
AtomicInteger dueRuleInteger, AtomicInteger inAdvanceRuleInteger) {
switch (paymentAllocationType.getDueType()) {
case PAST_DUE -> validateDueType(pastDueRuleInteger, dueRuleInteger, inAdvanceRuleInteger);
case DUE -> validateDueType(dueRuleInteger, pastDueRuleInteger, inAdvanceRuleInteger);
case IN_ADVANCE -> validateDueType(inAdvanceRuleInteger, pastDueRuleInteger, dueRuleInteger);
}
}
private void validateDueType(AtomicInteger currentRuleInteger, AtomicInteger otherRule1Integer, AtomicInteger otherRule2Integer) {
currentRuleInteger.incrementAndGet();
if ((otherRule1Integer.get() > 0 && otherRule1Integer.get() < 4) || (otherRule2Integer.get() > 0 && otherRule2Integer.get() < 4)) {
raiseValidationError("mixed.due.type.allocation.rules.are.not.supported.with.horizontal.installment.processing",
"Horizontal repayment schedule processing is not supporting mixed due type allocation rules!");
}
}
private boolean hasDuplicateTransactionTypes(List<LoanProductPaymentAllocationRule> rules) { private boolean hasDuplicateTransactionTypes(List<LoanProductPaymentAllocationRule> rules) {
return rules != null return rules != null
&& rules.stream().map(LoanProductPaymentAllocationRule::getTransactionType).distinct().toList().size() != rules.size(); && rules.stream().map(LoanProductPaymentAllocationRule::getTransactionType).distinct().toList().size() != rules.size();
@@ -62,6 +62,7 @@ import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData;
import org.apache.fineract.portfolio.floatingrates.domain.FloatingRate; import org.apache.fineract.portfolio.floatingrates.domain.FloatingRate;
import org.apache.fineract.portfolio.fund.domain.Fund; import org.apache.fineract.portfolio.fund.domain.Fund;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException; import org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException;
@@ -312,6 +313,15 @@ public class LoanProduct extends AbstractPersistableCustom {
loanScheduleType = LoanScheduleType.CUMULATIVE; loanScheduleType = LoanScheduleType.CUMULATIVE;
} }
final LoanScheduleProcessingType loanScheduleProcessingType;
if (LoanScheduleType.PROGRESSIVE.equals(loanScheduleType) && command.hasParameter("loanScheduleProcessingType")) {
loanScheduleProcessingType = LoanScheduleProcessingType
.valueOf(command.stringValueOfParameterNamed("loanScheduleProcessingType"));
} else {
// For backward compatibility
loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL;
}
final boolean useBorrowerCycle = command final boolean useBorrowerCycle = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME); .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME);
final Set<LoanProductBorrowerCycleVariations> loanProductBorrowerCycleVariations = new HashSet<>(); final Set<LoanProductBorrowerCycleVariations> loanProductBorrowerCycleVariations = new HashSet<>();
@@ -436,7 +446,7 @@ public class LoanProduct extends AbstractPersistableCustom {
disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber, disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber,
dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment,
enableAutoRepaymentForDownPayment, repaymentStartDateType, disableScheduleExtensionForDownPayment, enableAutoRepaymentForDownPayment, repaymentStartDateType, disableScheduleExtensionForDownPayment,
enableInstallmentLevelDelinquency, loanScheduleType); enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType);
} }
@@ -652,7 +662,7 @@ public class LoanProduct extends AbstractPersistableCustom {
final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
final boolean enableAutoRepaymentForDownPayment, final RepaymentStartDateType repaymentStartDateType, final boolean enableAutoRepaymentForDownPayment, final RepaymentStartDateType repaymentStartDateType,
final boolean disableScheduleExtensionForDownPayment, final boolean enableInstallmentLevelDelinquency, final boolean disableScheduleExtensionForDownPayment, final boolean enableInstallmentLevelDelinquency,
final LoanScheduleType loanScheduleType) { final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
this.fund = fund; this.fund = fund;
this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; this.transactionProcessingStrategyCode = transactionProcessingStrategyCode;
@@ -694,7 +704,7 @@ public class LoanProduct extends AbstractPersistableCustom {
recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod,
inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType.getValue(), daysInYearType.getValue(), inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType.getValue(), daysInYearType.getValue(),
isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment,
enableAutoRepaymentForDownPayment, disableScheduleExtensionForDownPayment, loanScheduleType); enableAutoRepaymentForDownPayment, disableScheduleExtensionForDownPayment, loanScheduleType, loanScheduleProcessingType);
this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings(); this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings();
@@ -39,6 +39,7 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
@@ -146,6 +147,10 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private LoanScheduleType loanScheduleType; private LoanScheduleType loanScheduleType;
@Column(name = "loan_schedule_processing_type", nullable = false)
@Enumerated(EnumType.STRING)
private LoanScheduleProcessingType loanScheduleProcessingType;
public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currency, final BigDecimal principal, public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currency, final BigDecimal principal,
final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType,
final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod, final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod,
@@ -157,7 +162,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization, final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization,
final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment, final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment,
final LoanScheduleType loanScheduleType) { final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType,
nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion, nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion,
@@ -165,7 +170,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod,
inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment,
disableScheduleExtensionForDownPayment, loanScheduleType); disableScheduleExtensionForDownPayment, loanScheduleType, loanScheduleProcessingType);
} }
protected LoanProductRelatedDetail() { protected LoanProductRelatedDetail() {
@@ -183,7 +188,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization, final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization,
final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment, final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment,
final LoanScheduleType loanScheduleType) { final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
this.currency = currency; this.currency = currency;
this.principal = defaultPrincipal; this.principal = defaultPrincipal;
this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod; this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod;
@@ -215,6 +220,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment; this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment;
this.disableScheduleExtensionForDownPayment = disableScheduleExtensionForDownPayment; this.disableScheduleExtensionForDownPayment = disableScheduleExtensionForDownPayment;
this.loanScheduleType = loanScheduleType; this.loanScheduleType = loanScheduleType;
this.loanScheduleProcessingType = loanScheduleProcessingType;
} }
private Integer defaultToNullIfZero(final Integer value) { private Integer defaultToNullIfZero(final Integer value) {
@@ -740,4 +746,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
public LoanScheduleType getLoanScheduleType() { public LoanScheduleType getLoanScheduleType() {
return loanScheduleType; return loanScheduleType;
} }
public LoanScheduleProcessingType getLoanScheduleProcessingType() {
return loanScheduleProcessingType;
}
} }
@@ -34,4 +34,5 @@
<include relativeToChangelogFile="true" file="parts/1009_refactor_loan_Installment_delinquency_tag.xml"/> <include relativeToChangelogFile="true" file="parts/1009_refactor_loan_Installment_delinquency_tag.xml"/>
<include relativeToChangelogFile="true" file="parts/1010_introduce_loan_schedule_type_configuration.xml"/> <include relativeToChangelogFile="true" file="parts/1010_introduce_loan_schedule_type_configuration.xml"/>
<include relativeToChangelogFile="true" file="parts/1011_add_delinquency_actions_table.xml"/> <include relativeToChangelogFile="true" file="parts/1011_add_delinquency_actions_table.xml"/>
<include relativeToChangelogFile="true" file="parts/1012_introduce_loan_schedule_processing_type_configuration.xml"/>
</databaseChangeLog> </databaseChangeLog>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_product_loan">
<column defaultValue="HORIZONTAL" type="varchar(20)" name="loan_schedule_processing_type">
<constraints nullable="false"/>
</column>
</addColumn>
<addColumn tableName="m_loan">
<column defaultValue="HORIZONTAL" type="varchar(20)" name="loan_schedule_processing_type">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -143,11 +143,37 @@ class AdvancedPaymentAllocationsValidatorTest {
() -> underTest.validate(List.of(lppr1, lppr2), ADVANCED_PAYMENT_ALLOCATION_STRATEGY)); () -> underTest.validate(List.of(lppr1, lppr2), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
} }
@Test
public void checkGroupingOfAllocationRulesGoodOrder() {
List<LoanProductPaymentAllocationRule> allocationRules1 = List.of(createLoanProductAllocationRule1());
Assertions.assertDoesNotThrow(() -> underTest.checkGroupingOfAllocationRules(allocationRules1));
List<LoanProductPaymentAllocationRule> allocationRules2 = List.of(createLoanProductAllocationRule4());
Assertions.assertDoesNotThrow(() -> underTest.checkGroupingOfAllocationRules(allocationRules2));
}
@Test
public void checkGroupingOfAllocationRulesWrongOrder() {
List<LoanProductPaymentAllocationRule> allocationRules = List.of(createLoanProductAllocationRule3());
assertPlatformValidationException("Horizontal repayment schedule processing is not supporting mixed due type allocation rules!",
"mixed.due.type.allocation.rules.are.not.supported.with.horizontal.installment.processing",
() -> underTest.checkGroupingOfAllocationRules(allocationRules));
}
private void assertPlatformValidationException(String message, String code, Executable executable) { private void assertPlatformValidationException(String message, String code, Executable executable) {
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, executable); PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, executable);
assertPlatformException(message, code, validationException); assertPlatformException(message, code, validationException);
} }
@NotNull
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule1() {
LoanProductPaymentAllocationRule lppr1 = new LoanProductPaymentAllocationRule();
lppr1.setTransactionType(DEFAULT);
lppr1.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
lppr1.setAllocationTypes(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
return lppr1;
}
@NotNull @NotNull
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule2() { private static LoanProductPaymentAllocationRule createLoanProductAllocationRule2() {
LoanProductPaymentAllocationRule lppr2 = new LoanProductPaymentAllocationRule(); LoanProductPaymentAllocationRule lppr2 = new LoanProductPaymentAllocationRule();
@@ -160,20 +186,36 @@ class AdvancedPaymentAllocationsValidatorTest {
} }
@NotNull @NotNull
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule1() { private static LoanProductPaymentAllocationRule createLoanProductAllocationRule3() {
LoanProductPaymentAllocationRule lppr1 = new LoanProductPaymentAllocationRule(); LoanProductPaymentAllocationRule lppr = new LoanProductPaymentAllocationRule();
lppr1.setTransactionType(DEFAULT); lppr.setTransactionType(REPAYMENT);
lppr1.setFutureInstallmentAllocationRule(LAST_INSTALLMENT); lppr.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
lppr1.setAllocationTypes(EnumSet.allOf(PaymentAllocationType.class).stream().toList()); ArrayList<PaymentAllocationType> allocationTypes = new ArrayList<>(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
return lppr1; Collections.swap(allocationTypes, 0, 4);
Collections.swap(allocationTypes, 3, 8);
Collections.swap(allocationTypes, 7, 11);
lppr.setAllocationTypes(allocationTypes);
return lppr;
}
@NotNull
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule4() {
LoanProductPaymentAllocationRule lppr = new LoanProductPaymentAllocationRule();
lppr.setTransactionType(DEFAULT);
lppr.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
ArrayList<PaymentAllocationType> allocationTypes = new ArrayList<>(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
Collections.swap(allocationTypes, 0, 4);
Collections.swap(allocationTypes, 1, 5);
Collections.swap(allocationTypes, 2, 6);
Collections.swap(allocationTypes, 3, 7);
lppr.setAllocationTypes(allocationTypes);
return lppr;
} }
@NotNull @NotNull
private static List<Pair<Integer, PaymentAllocationType>> createPaymentAllocationTypeList() { private static List<Pair<Integer, PaymentAllocationType>> createPaymentAllocationTypeList() {
AtomicInteger i = new AtomicInteger(1); AtomicInteger i = new AtomicInteger(1);
List<Pair<Integer, PaymentAllocationType>> list = EnumSet.allOf(PaymentAllocationType.class).stream() return EnumSet.allOf(PaymentAllocationType.class).stream().map(p -> Pair.of(i.getAndIncrement(), p)).toList();
.map(p -> Pair.of(i.getAndIncrement(), p)).toList();
return list;
} }
private void assertPlatformException(String expectedMessage, String expectedCode, private void assertPlatformException(String expectedMessage, String expectedCode,
@@ -136,6 +136,8 @@ import org.apache.fineract.portfolio.loanaccount.guarantor.data.GuarantorData;
import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorReadPlatformService; import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryReadPlatformService; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService;
@@ -1060,8 +1062,8 @@ public class LoansApiResource {
repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions,
interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, allowedLoanOfficers, loanPurposeOptions, interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, allowedLoanOfficers, loanPurposeOptions,
loanCollateralOptions, calendarOptions, notes, accountLinkingOptions, linkedAccount, disbursementData, emiAmountVariations, loanCollateralOptions, calendarOptions, notes, accountLinkingOptions, linkedAccount, disbursementData, emiAmountVariations,
overdueCharges, paidInAdvanceTemplate, interestRatesPeriods, clientActiveLoanOptions, rates, isRatesEnabled, overdueCharges, paidInAdvanceTemplate, interestRatesPeriods, clientActiveLoanOptions, rates, isRatesEnabled, collectionData,
collectionData); LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList());
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(), final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(),
mandatoryResponseParameters); mandatoryResponseParameters);
@@ -23,6 +23,7 @@ import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiResourceSwagger.GetDelinquencyRangesResponse; import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiResourceSwagger.GetDelinquencyRangesResponse;
/** /**
@@ -1086,6 +1087,10 @@ final class LoansApiResourceSwagger {
public Boolean enableAutoRepaymentForDownPayment; public Boolean enableAutoRepaymentForDownPayment;
@Schema(example = "false") @Schema(example = "false")
public Boolean disableScheduleExtensionForDownPayment; public Boolean disableScheduleExtensionForDownPayment;
@Schema(example = "CUMULATIVE")
public EnumOptionData loanScheduleType;
@Schema(example = "HORIZONTAL")
public EnumOptionData loanScheduleProcessingType;
} }
@Schema(description = "GetLoansResponse") @Schema(description = "GetLoansResponse")
@@ -1169,6 +1174,10 @@ final class LoansApiResourceSwagger {
public Integer graceOnInterestPayment; public Integer graceOnInterestPayment;
@Schema(example = "1") @Schema(example = "1")
public Integer graceOnArrearsAgeing; public Integer graceOnArrearsAgeing;
@Schema(example = "CUMULATIVE")
public String loanScheduleType;
@Schema(example = "HORIZONTAL")
public String loanScheduleProcessingType;
} }
@Schema(description = "PostLoansResponse") @Schema(description = "PostLoansResponse")
@@ -1312,6 +1321,10 @@ final class LoansApiResourceSwagger {
public List<PutLoansLoanIdChanges> charges; public List<PutLoansLoanIdChanges> charges;
public List<PutLoansLoanIdCollateral> collateral; public List<PutLoansLoanIdCollateral> collateral;
public List<PutLoansLoanIdDisbursementData> disbursementData; public List<PutLoansLoanIdDisbursementData> disbursementData;
@Schema(example = "CUMULATIVE")
public String loanScheduleType;
@Schema(example = "HORIZONTAL")
public String loanScheduleProcessingType;
static final class PutLoansLoanIdChanges { static final class PutLoansLoanIdChanges {
@@ -166,6 +166,8 @@ public class LoanAccountData {
private Collection<ChargeData> chargeOptions; private Collection<ChargeData> chargeOptions;
private Collection<CodeValueData> loanCollateralOptions; private Collection<CodeValueData> loanCollateralOptions;
private Collection<CalendarData> calendarOptions; private Collection<CalendarData> calendarOptions;
private List<EnumOptionData> loanScheduleTypeOptions;
private List<EnumOptionData> loanScheduleProcessingTypeOptions;
@Transient @Transient
private BigDecimal feeChargesAtDisbursementCharged; private BigDecimal feeChargesAtDisbursementCharged;
@@ -262,6 +264,9 @@ public class LoanAccountData {
private Boolean enableAutoRepaymentForDownPayment; private Boolean enableAutoRepaymentForDownPayment;
private Boolean disableScheduleExtensionForDownPayment; private Boolean disableScheduleExtensionForDownPayment;
private EnumOptionData loanScheduleType;
private EnumOptionData loanScheduleProcessingType;
public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEnumOption, Long clientId, Long productId, public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEnumOption, Long clientId, Long productId,
Long loanOfficerId, LocalDate submittedOnDate, Long fundId, BigDecimal principal, Integer numberOfRepayments, Long loanOfficerId, LocalDate submittedOnDate, Long fundId, BigDecimal principal, Integer numberOfRepayments,
Integer repaymentEvery, EnumOptionData repaidEveryFrequencyEnums, Integer loanTermFrequency, Integer repaymentEvery, EnumOptionData repaidEveryFrequencyEnums, Integer loanTermFrequency,
@@ -389,7 +394,10 @@ public class LoanAccountData {
.setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled) .setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent) .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements) .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
.setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff); .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff)
.setLoanScheduleType(acc.loanScheduleType).setLoanScheduleProcessingType(acc.loanScheduleProcessingType)
.setLoanScheduleTypeOptions(acc.loanScheduleTypeOptions)
.setLoanScheduleProcessingTypeOptions(acc.loanScheduleProcessingTypeOptions);
} }
/** /**
@@ -460,7 +468,10 @@ public class LoanAccountData {
.setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled) .setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent) .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements) .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
.setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff); .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff)
.setLoanScheduleType(acc.loanScheduleType).setLoanScheduleProcessingType(acc.loanScheduleProcessingType)
.setLoanScheduleTypeOptions(acc.loanScheduleTypeOptions)
.setLoanScheduleProcessingTypeOptions(acc.loanScheduleProcessingTypeOptions);
} }
public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product, public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product,
@@ -472,7 +483,8 @@ public class LoanAccountData {
final Collection<EnumOptionData> interestTypeOptions, final Collection<EnumOptionData> interestCalculationPeriodTypeOptions, final Collection<EnumOptionData> interestTypeOptions, final Collection<EnumOptionData> interestCalculationPeriodTypeOptions,
final Collection<FundData> fundOptions, final Collection<ChargeData> chargeOptions, final Collection<FundData> fundOptions, final Collection<ChargeData> chargeOptions,
final Collection<CodeValueData> loanPurposeOptions, final Collection<CodeValueData> loanCollateralOptions, final Collection<CodeValueData> loanPurposeOptions, final Collection<CodeValueData> loanCollateralOptions,
final Integer loanCycleNumber, final Collection<LoanAccountSummaryData> clientActiveLoanOptions) { final Integer loanCycleNumber, final Collection<LoanAccountSummaryData> clientActiveLoanOptions,
final List<EnumOptionData> loanScheduleTypeOptions, final List<EnumOptionData> loanScheduleProcessingTypeOptions) {
final EnumOptionData termPeriodFrequencyType = product.getRepaymentFrequencyType(); final EnumOptionData termPeriodFrequencyType = product.getRepaymentFrequencyType();
@@ -562,7 +574,10 @@ public class LoanAccountData {
.setMaximumGap(product.getMaximumGap()).setTopup(product.isCanUseForTopup()) .setMaximumGap(product.getMaximumGap()).setTopup(product.isCanUseForTopup())
.setClientActiveLoanOptions(clientActiveLoanOptions).setIsEqualAmortization(product.isEqualAmortization()) .setClientActiveLoanOptions(clientActiveLoanOptions).setIsEqualAmortization(product.isEqualAmortization())
.setFixedPrincipalPercentagePerInstallment(product.getFixedPrincipalPercentagePerInstallment()) .setFixedPrincipalPercentagePerInstallment(product.getFixedPrincipalPercentagePerInstallment())
.setDelinquent(CollectionData.template()).setDisallowExpectedDisbursements(product.getDisallowExpectedDisbursements()); .setDelinquent(CollectionData.template()).setDisallowExpectedDisbursements(product.getDisallowExpectedDisbursements())
.setLoanScheduleType(product.getLoanScheduleType()).setLoanScheduleProcessingType(product.getLoanScheduleProcessingType())
.setLoanScheduleTypeOptions(loanScheduleTypeOptions)
.setLoanScheduleProcessingTypeOptions(loanScheduleProcessingTypeOptions);
} }
public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) { public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) {
@@ -628,7 +643,8 @@ public class LoanAccountData {
.setIsEqualAmortization(product.isEqualAmortization()).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled) .setIsEqualAmortization(product.isEqualAmortization()).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
.setFixedPrincipalPercentagePerInstallment(product.getFixedPrincipalPercentagePerInstallment()).setDelinquent(delinquent) .setFixedPrincipalPercentagePerInstallment(product.getFixedPrincipalPercentagePerInstallment()).setDelinquent(delinquent)
.setDisallowExpectedDisbursements(product.getDisallowExpectedDisbursements()).setFraud(acc.fraud) .setDisallowExpectedDisbursements(product.getDisallowExpectedDisbursements()).setFraud(acc.fraud)
.setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff); .setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff).setLoanScheduleType(acc.getLoanScheduleType())
.setLoanScheduleProcessingType(acc.getLoanScheduleProcessingType());
} }
/* /*
@@ -664,7 +680,8 @@ public class LoanAccountData {
final DelinquencyRangeData delinquencyRange, final boolean disallowExpectedDisbursements, final boolean fraud, final DelinquencyRangeData delinquencyRange, final boolean disallowExpectedDisbursements, final boolean fraud,
LocalDate lastClosedBusinessDate, LocalDate overpaidOnDate, final boolean chargedOff, final boolean enableDownPayment, LocalDate lastClosedBusinessDate, LocalDate overpaidOnDate, final boolean chargedOff, final boolean enableDownPayment,
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment,
final boolean disableScheduleExtensionForDownPayment, final boolean enableInstallmentLevelDelinquency) { final boolean disableScheduleExtensionForDownPayment, final boolean enableInstallmentLevelDelinquency,
final EnumOptionData loanScheduleType, final EnumOptionData loanScheduleProcessingType) {
final CollectionData delinquent = CollectionData.template(); final CollectionData delinquent = CollectionData.template();
@@ -708,7 +725,8 @@ public class LoanAccountData {
.setEnableDownPayment(enableDownPayment).setDisbursedAmountPercentageForDownPayment(disbursedAmountPercentageForDownPayment) .setEnableDownPayment(enableDownPayment).setDisbursedAmountPercentageForDownPayment(disbursedAmountPercentageForDownPayment)
.setEnableAutoRepaymentForDownPayment(enableAutoRepaymentForDownPayment) .setEnableAutoRepaymentForDownPayment(enableAutoRepaymentForDownPayment)
.setDisableScheduleExtensionForDownPayment(disableScheduleExtensionForDownPayment) .setDisableScheduleExtensionForDownPayment(disableScheduleExtensionForDownPayment)
.setEnableInstallmentLevelDelinquency(enableInstallmentLevelDelinquency); .setEnableInstallmentLevelDelinquency(enableInstallmentLevelDelinquency).setLoanScheduleType(loanScheduleType)
.setLoanScheduleProcessingType(loanScheduleProcessingType);
} }
/* /*
@@ -732,7 +750,8 @@ public class LoanAccountData {
final Collection<LoanTermVariationsData> emiAmountVariations, final Collection<ChargeData> overdueCharges, final Collection<LoanTermVariationsData> emiAmountVariations, final Collection<ChargeData> overdueCharges,
final PaidInAdvanceData paidInAdvance, Collection<InterestRatePeriodData> interestRatesPeriods, final PaidInAdvanceData paidInAdvance, Collection<InterestRatePeriodData> interestRatesPeriods,
final Collection<LoanAccountSummaryData> clientActiveLoanOptions, final List<RateData> rates, final Boolean isRatesEnabled, final Collection<LoanAccountSummaryData> clientActiveLoanOptions, final List<RateData> rates, final Boolean isRatesEnabled,
final CollectionData delinquent) { final CollectionData delinquent, final List<EnumOptionData> loanScheduleTypeOptions,
final List<EnumOptionData> loanScheduleProcessingTypeOptions) {
// TODO: why are these variables 'calendarData', 'chargeTemplate' never used (see original private constructor) // TODO: why are these variables 'calendarData', 'chargeTemplate' never used (see original private constructor)
@@ -797,7 +816,9 @@ public class LoanAccountData {
.setDisbursedAmountPercentageForDownPayment(acc.disbursedAmountPercentageForDownPayment) .setDisbursedAmountPercentageForDownPayment(acc.disbursedAmountPercentageForDownPayment)
.setEnableAutoRepaymentForDownPayment(acc.enableAutoRepaymentForDownPayment) .setEnableAutoRepaymentForDownPayment(acc.enableAutoRepaymentForDownPayment)
.setDisableScheduleExtensionForDownPayment(acc.disableScheduleExtensionForDownPayment) .setDisableScheduleExtensionForDownPayment(acc.disableScheduleExtensionForDownPayment)
.setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency); .setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency).setLoanScheduleType(acc.loanScheduleType)
.setLoanScheduleProcessingType(acc.loanScheduleProcessingType).setLoanScheduleTypeOptions(loanScheduleTypeOptions)
.setLoanScheduleProcessingTypeOptions(loanScheduleProcessingTypeOptions);
} }
public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection<LoanProductData> productOptions, public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection<LoanProductData> productOptions,
@@ -811,7 +832,8 @@ public class LoanAccountData {
acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null, acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null,
allowedLoanOfficers, acc.loanPurposeOptions, acc.loanCollateralOptions, calendarOptions, acc.notes, accountLinkingOptions, allowedLoanOfficers, acc.loanPurposeOptions, acc.loanCollateralOptions, calendarOptions, acc.notes, accountLinkingOptions,
acc.linkedAccount, acc.disbursementDetails, acc.emiAmountVariations, acc.overdueCharges, acc.paidInAdvance, acc.linkedAccount, acc.disbursementDetails, acc.emiAmountVariations, acc.overdueCharges, acc.paidInAdvance,
acc.interestRatesPeriods, acc.clientActiveLoanOptions, acc.rates, isRatesEnabled, acc.delinquent); acc.interestRatesPeriods, acc.clientActiveLoanOptions, acc.rates, isRatesEnabled, acc.delinquent,
acc.loanScheduleTypeOptions, acc.loanScheduleProcessingTypeOptions);
} }
public static LoanAccountData associateGroup(final LoanAccountData acc, final GroupGeneralData group) { public static LoanAccountData associateGroup(final LoanAccountData acc, final GroupGeneralData group) {
@@ -873,7 +895,9 @@ public class LoanAccountData {
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent) .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements) .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
.setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff) .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff)
.setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency); .setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency).setLoanScheduleType(acc.loanScheduleType)
.setLoanScheduleProcessingType(acc.loanScheduleProcessingType).setLoanScheduleTypeOptions(acc.loanScheduleTypeOptions)
.setLoanScheduleProcessingTypeOptions(acc.loanScheduleProcessingTypeOptions);
} }
public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map<Long, Integer> memberLoanCycle) { public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map<Long, Integer> memberLoanCycle) {
@@ -971,7 +995,9 @@ public class LoanAccountData {
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent) .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements) .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
.setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff) .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff)
.setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency); .setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency).setLoanScheduleType(acc.loanScheduleType)
.setLoanScheduleProcessingType(acc.loanScheduleProcessingType).setLoanScheduleTypeOptions(acc.loanScheduleTypeOptions)
.setLoanScheduleProcessingTypeOptions(acc.loanScheduleProcessingTypeOptions);
} }
public static LoanAccountData withInterestRecalculationCalendarData(final LoanAccountData acc, final CalendarData calendarData, public static LoanAccountData withInterestRecalculationCalendarData(final LoanAccountData acc, final CalendarData calendarData,
@@ -1037,7 +1063,9 @@ public class LoanAccountData {
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent) .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements) .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
.setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff) .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff)
.setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency); .setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency).setLoanScheduleType(acc.loanScheduleType)
.setLoanScheduleProcessingType(acc.loanScheduleProcessingType).setLoanScheduleTypeOptions(acc.loanScheduleTypeOptions)
.setLoanScheduleProcessingTypeOptions(acc.loanScheduleProcessingTypeOptions);
} }
public static LoanAccountData withLoanCalendarData(final LoanAccountData acc, final CalendarData calendarData) { public static LoanAccountData withLoanCalendarData(final LoanAccountData acc, final CalendarData calendarData) {
@@ -1096,7 +1124,9 @@ public class LoanAccountData {
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent) .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements) .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
.setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff) .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff)
.setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency); .setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency).setLoanScheduleType(acc.loanScheduleType)
.setLoanScheduleProcessingType(acc.loanScheduleProcessingType).setLoanScheduleTypeOptions(acc.loanScheduleTypeOptions)
.setLoanScheduleProcessingTypeOptions(acc.loanScheduleProcessingTypeOptions);
} }
public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) { public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) {
@@ -1158,7 +1188,9 @@ public class LoanAccountData {
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent) .setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements) .setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
.setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff) .setFraud(acc.fraud).setOverpaidOnDate(acc.overpaidOnDate).setChargedOff(acc.chargedOff)
.setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency); .setEnableInstallmentLevelDelinquency(acc.enableInstallmentLevelDelinquency).setLoanScheduleType(acc.loanScheduleType)
.setLoanScheduleProcessingType(acc.loanScheduleProcessingType).setLoanScheduleTypeOptions(acc.loanScheduleTypeOptions)
.setLoanScheduleProcessingTypeOptions(acc.loanScheduleProcessingTypeOptions);
} }
public static final Comparator<LoanAccountData> ClientNameComparator = (loan1, loan2) -> { public static final Comparator<LoanAccountData> ClientNameComparator = (loan1, loan2) -> {
@@ -98,6 +98,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplica
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator; import org.apache.fineract.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler;
@@ -472,7 +473,17 @@ public class LoanScheduleAssembler {
.isDisableScheduleExtensionForDownPayment(); .isDisableScheduleExtensionForDownPayment();
} }
LoanScheduleType loanScheduleType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleType(); LoanScheduleType loanScheduleType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleType();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_TYPE, element)) {
LoanScheduleType.valueOf(this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_TYPE, element));
}
LoanScheduleProcessingType loanScheduleProcessingType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleProcessingType();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_TYPE, element)) {
LoanScheduleProcessingType
.valueOf(this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element));
}
return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
repaymentEvery, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod, repaymentEvery, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod,
interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
@@ -487,7 +498,7 @@ public class LoanScheduleAssembler {
allowCompoundingOnEod, isEqualAmortization, isInterestToBeRecoveredFirstWhenGreaterThanEMI, allowCompoundingOnEod, isEqualAmortization, isInterestToBeRecoveredFirstWhenGreaterThanEMI,
fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled,
disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate,
isScheduleExtensionForDownPaymentDisabled, loanScheduleType); isScheduleExtensionForDownPaymentDisabled, loanScheduleType, loanScheduleProcessingType);
} }
private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery, private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery,
@@ -49,13 +49,18 @@ import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollatera
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollateralQuantity; import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollateralQuantity;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollaterals; import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollaterals;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.domain.AdvancedPaymentAllocationsValidator;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException; import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -99,7 +104,8 @@ public final class LoanApplicationCommandFromApiJsonHelper {
LoanApiConstants.applicationId, // glim specific LoanApiConstants.applicationId, // glim specific
LoanApiConstants.lastApplication, // glim specific LoanApiConstants.lastApplication, // glim specific
LoanApiConstants.daysInYearTypeParameterName, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanApiConstants.daysInYearTypeParameterName, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName,
LoanApiConstants.DISALLOW_EXPECTED_DISBURSEMENTS, LoanApiConstants.FRAUD_ATTRIBUTE_NAME)); LoanApiConstants.DISALLOW_EXPECTED_DISBURSEMENTS, LoanApiConstants.FRAUD_ATTRIBUTE_NAME,
LoanProductConstants.LOAN_SCHEDULE_TYPE, LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE));
public static final String LOANAPPLICATION_UNDO = "loanapplication.undo"; public static final String LOANAPPLICATION_UNDO = "loanapplication.undo";
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
@@ -107,6 +113,7 @@ public final class LoanApplicationCommandFromApiJsonHelper {
private final ClientCollateralManagementRepositoryWrapper clientCollateralManagementRepositoryWrapper; private final ClientCollateralManagementRepositoryWrapper clientCollateralManagementRepositoryWrapper;
private final LoanChargeApiJsonValidator loanChargeApiJsonValidator; private final LoanChargeApiJsonValidator loanChargeApiJsonValidator;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator;
public void validateForCreate(final String json, final boolean isMeetingMandatoryForJLGLoans, final LoanProduct loanProduct) { public void validateForCreate(final String json, final boolean isMeetingMandatoryForJLGLoans, final LoanProduct loanProduct) {
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
@@ -543,6 +550,33 @@ public final class LoanApplicationCommandFromApiJsonHelper {
} }
validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal);
final String loanScheduleType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_TYPE, element);
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_TYPE).value(loanScheduleType).ignoreIfNull()
.isOneOfEnumValues(LoanScheduleType.class);
String loanScheduleProcessingType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleProcessingType().name();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) {
loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE,
element);
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType)
.isOneOfEnumValues(LoanScheduleProcessingType.class);
}
if (LoanScheduleProcessingType.VERTICAL.equals(LoanScheduleProcessingType.valueOf(loanScheduleProcessingType))
&& !AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY
.equals(transactionProcessingStrategy)) {
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).failWithCode(
"supported.only.with.advanced.payment.allocation.strategy",
"Vertical repayment schedule processing is only available with `Advanced payment allocation` strategy");
}
List<LoanProductPaymentAllocationRule> allocationRules = loanProduct.getPaymentAllocationRules();
if (LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType)
&& AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategy)) {
advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(allocationRules);
}
validatePartialPeriodSupport(interestCalculationPeriodType, baseDataValidator, element, loanProduct); validatePartialPeriodSupport(interestCalculationPeriodType, baseDataValidator, element, loanProduct);
if (!dataValidationErrors.isEmpty()) { if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors); throw new PlatformApiDataValidationException(dataValidationErrors);
@@ -625,9 +659,10 @@ public final class LoanApplicationCommandFromApiJsonHelper {
.integerGreaterThanZero(); .integerGreaterThanZero();
} }
String transactionProcessingStrategy = existingLoanApplication.getTransactionProcessingStrategyCode();
if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element)) { if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element)) {
atLeastOneParameterPassedForUpdate = true; atLeastOneParameterPassedForUpdate = true;
final String transactionProcessingStrategy = this.fromApiJsonHelper transactionProcessingStrategy = this.fromApiJsonHelper
.extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName) baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName)
.value(transactionProcessingStrategy).notNull(); .value(transactionProcessingStrategy).notNull();
@@ -1049,6 +1084,34 @@ public final class LoanApplicationCommandFromApiJsonHelper {
validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal); validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal);
validatePartialPeriodSupport(interestCalculationPeriodType, baseDataValidator, element, loanProduct); validatePartialPeriodSupport(interestCalculationPeriodType, baseDataValidator, element, loanProduct);
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_TYPE, element)) {
final String loanScheduleType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_TYPE, element);
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_TYPE).value(loanScheduleType).ignoreIfNull()
.isOneOfEnumValues(LoanScheduleType.class);
}
String loanScheduleProcessingType = existingLoanApplication.getLoanRepaymentScheduleDetail().getLoanScheduleProcessingType().name();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) {
loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE,
element);
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType)
.ignoreIfNull().isOneOfEnumValues(LoanScheduleProcessingType.class);
}
if (LoanScheduleProcessingType.VERTICAL.equals(LoanScheduleProcessingType.valueOf(loanScheduleProcessingType))
&& !AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY
.equals(transactionProcessingStrategy)) {
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).failWithCode(
"supported.only.with.advanced.payment.allocation.strategy",
"Vertical repayment schedule processing is only available with `Advanced payment allocation` strategy");
}
List<LoanProductPaymentAllocationRule> allocationRules = loanProduct.getPaymentAllocationRules();
if (LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType)
&& AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategy)) {
advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(allocationRules);
}
if (!dataValidationErrors.isEmpty()) { if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors); dataValidationErrors);
@@ -123,6 +123,8 @@ import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFou
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanTransactionRelationMapper; import org.apache.fineract.portfolio.loanaccount.mapper.LoanTransactionRelationMapper;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.data.LoanProductData;
import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData; import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData;
@@ -696,7 +698,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
+ " l.total_recovered_derived as totalRecovered, topuploan.account_no as closureLoanAccountNo, " + " l.total_recovered_derived as totalRecovered, topuploan.account_no as closureLoanAccountNo, "
+ " topup.topup_amount as topupAmount, l.last_closed_business_date as lastClosedBusinessDate,l.overpaidon_date as overpaidOnDate, " + " topup.topup_amount as topupAmount, l.last_closed_business_date as lastClosedBusinessDate,l.overpaidon_date as overpaidOnDate, "
+ " l.is_charged_off as isChargedOff, l.charge_off_reason_cv_id as chargeOffReasonId, codec.code_value as chargeOffReason, l.charged_off_on_date as chargedOffOnDate, l.enable_down_payment as enableDownPayment, l.disbursed_amount_percentage_for_down_payment as disbursedAmountPercentageForDownPayment, l.enable_auto_repayment_for_down_payment as enableAutoRepaymentForDownPayment, l.disable_schedule_extension_for_down_payment as disableScheduleExtensionForDownPayment," + " l.is_charged_off as isChargedOff, l.charge_off_reason_cv_id as chargeOffReasonId, codec.code_value as chargeOffReason, l.charged_off_on_date as chargedOffOnDate, l.enable_down_payment as enableDownPayment, l.disbursed_amount_percentage_for_down_payment as disbursedAmountPercentageForDownPayment, l.enable_auto_repayment_for_down_payment as enableAutoRepaymentForDownPayment, l.disable_schedule_extension_for_down_payment as disableScheduleExtensionForDownPayment,"
+ " cobu.username as chargedOffByUsername, cobu.firstname as chargedOffByFirstname, cobu.lastname as chargedOffByLastname " + " cobu.username as chargedOffByUsername, cobu.firstname as chargedOffByFirstname, cobu.lastname as chargedOffByLastname, l.loan_schedule_type as loanScheduleType, l.loan_schedule_processing_type as loanScheduleProcessingType "
+ " from m_loan l" // + " from m_loan l" //
+ " join m_product_loan lp on lp.id = l.product_id" // + " join m_product_loan lp on lp.id = l.product_id" //
+ " left join m_loan_recalculation_details lir on lir.loan_id = l.id join m_currency rc on rc." + " left join m_loan_recalculation_details lir on lir.loan_id = l.id join m_currency rc on rc."
@@ -1044,6 +1046,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final boolean enableAutoRepaymentForDownPayment = rs.getBoolean("enableAutoRepaymentForDownPayment"); final boolean enableAutoRepaymentForDownPayment = rs.getBoolean("enableAutoRepaymentForDownPayment");
final boolean disableScheduleExtensionForDownPayment = rs.getBoolean("disableScheduleExtensionForDownPayment"); final boolean disableScheduleExtensionForDownPayment = rs.getBoolean("disableScheduleExtensionForDownPayment");
final boolean enableInstallmentLevelDelinquency = rs.getBoolean("enableInstallmentLevelDelinquency"); final boolean enableInstallmentLevelDelinquency = rs.getBoolean("enableInstallmentLevelDelinquency");
final String loanScheduleTypeStr = rs.getString("loanScheduleType");
final LoanScheduleType loanScheduleType = LoanScheduleType.valueOf(loanScheduleTypeStr);
final String loanScheduleProcessingTypeStr = rs.getString("loanScheduleProcessingType");
final LoanScheduleProcessingType loanScheduleProcessingType = LoanScheduleProcessingType.valueOf(loanScheduleProcessingTypeStr);
return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientAccountNo, clientName,
clientOfficeId, clientExternalId, groupData, loanType, loanProductId, loanProductName, loanProductDescription, clientOfficeId, clientExternalId, groupData, loanType, loanProductId, loanProductName, loanProductDescription,
@@ -1061,7 +1067,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
canUseForTopup, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization, canUseForTopup, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization,
fixedPrincipalPercentagePerInstallment, delinquencyRange, disallowExpectedDisbursements, isFraud, fixedPrincipalPercentagePerInstallment, delinquencyRange, disallowExpectedDisbursements, isFraud,
lastClosedBusinessDate, overpaidOnDate, isChargedOff, enableDownPayment, disbursedAmountPercentageForDownPayment, lastClosedBusinessDate, overpaidOnDate, isChargedOff, enableDownPayment, disbursedAmountPercentageForDownPayment,
enableAutoRepaymentForDownPayment, disableScheduleExtensionForDownPayment, enableInstallmentLevelDelinquency); enableAutoRepaymentForDownPayment, disableScheduleExtensionForDownPayment, enableInstallmentLevelDelinquency,
loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData());
} }
} }
@@ -1552,7 +1559,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
return LoanAccountData.loanProductWithTemplateDefaults(loanProduct, loanTermFrequencyTypeOptions, repaymentFrequencyTypeOptions, return LoanAccountData.loanProductWithTemplateDefaults(loanProduct, loanTermFrequencyTypeOptions, repaymentFrequencyTypeOptions,
repaymentFrequencyNthDayTypeOptions, repaymentFrequencyDaysOfWeekTypeOptions, repaymentStrategyOptions, repaymentFrequencyNthDayTypeOptions, repaymentFrequencyDaysOfWeekTypeOptions, repaymentStrategyOptions,
interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions,
fundOptions, chargeOptions, loanPurposeOptions, loanCollateralOptions, loanCycleCounter, activeLoanOptions); fundOptions, chargeOptions, loanPurposeOptions, loanCollateralOptions, loanCycleCounter, activeLoanOptions,
LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList());
} }
@Override @Override
@@ -76,6 +76,8 @@ import org.apache.fineract.portfolio.floatingrates.service.FloatingRatesReadPlat
import org.apache.fineract.portfolio.fund.data.FundData; import org.apache.fineract.portfolio.fund.data.FundData;
import org.apache.fineract.portfolio.fund.service.FundReadPlatformService; import org.apache.fineract.portfolio.fund.service.FundReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.data.LoanProductData;
import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData; import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData;
@@ -424,7 +426,8 @@ public class LoanProductsApiResource {
preCloseInterestCalculationStrategyOptions, floatingRateOptions, interestRecalculationNthDayTypeOptions, preCloseInterestCalculationStrategyOptions, floatingRateOptions, interestRecalculationNthDayTypeOptions,
interestRecalculationDayOfWeekTypeOptions, isRatesEnabled, delinquencyBucketOptions, repaymentStartDateTypeOptions, interestRecalculationDayOfWeekTypeOptions, isRatesEnabled, delinquencyBucketOptions, repaymentStartDateTypeOptions,
advancedPaymentAllocationTransactionTypes, advancedPaymentAllocationFutureInstallmentAllocationRules, advancedPaymentAllocationTransactionTypes, advancedPaymentAllocationFutureInstallmentAllocationRules,
advancedPaymentAllocationTypes); advancedPaymentAllocationTypes, LoanScheduleType.getValuesAsEnumOptionDataList(),
LoanScheduleProcessingType.getValuesAsEnumOptionDataList());
} }
} }
@@ -258,9 +258,10 @@ final class LoanProductsApiResourceSwagger {
public AllowAttributeOverrides allowAttributeOverrides; public AllowAttributeOverrides allowAttributeOverrides;
public List<RateData> rates; public List<RateData> rates;
@Schema(example = "CUMULATIVE") @Schema(example = "CUMULATIVE")
public String loanScheduleType; public String loanScheduleType;
@Schema(example = "HORIZONTAL")
public String loanScheduleProcessingType;
static final class AllowAttributeOverrides { static final class AllowAttributeOverrides {
@@ -282,8 +283,6 @@ final class LoanProductsApiResourceSwagger {
public boolean graceOnPrincipalAndInterestPayment; public boolean graceOnPrincipalAndInterestPayment;
@Schema(example = "true") @Schema(example = "true")
public boolean graceOnArrearsAgeing; public boolean graceOnArrearsAgeing;
@Schema(example = "CUMULATIVE")
public String loanScheduleType;
} }
static final class ChargeData { static final class ChargeData {
@@ -1043,6 +1042,8 @@ final class LoanProductsApiResourceSwagger {
public List<EnumOptionData> advancedPaymentAllocationTransactionTypes; public List<EnumOptionData> advancedPaymentAllocationTransactionTypes;
public List<EnumOptionData> advancedPaymentAllocationFutureInstallmentAllocationRules; public List<EnumOptionData> advancedPaymentAllocationFutureInstallmentAllocationRules;
public List<EnumOptionData> advancedPaymentAllocationTypes; public List<EnumOptionData> advancedPaymentAllocationTypes;
public List<EnumOptionData> loanScheduleTypeOptions;
public List<EnumOptionData> loanScheduleProcessingTypeOptions;
} }
@Schema(description = "GetLoanProductsProductIdResponse") @Schema(description = "GetLoanProductsProductIdResponse")
@@ -1263,6 +1264,10 @@ final class LoanProductsApiResourceSwagger {
public GetLoanProductsRepaymentStartDateType repaymentStartDateType; public GetLoanProductsRepaymentStartDateType repaymentStartDateType;
@Schema(example = "false") @Schema(example = "false")
public Boolean disableScheduleExtensionForDownPayment; public Boolean disableScheduleExtensionForDownPayment;
@Schema(example = "CUMULATIVE")
public EnumOptionData loanScheduleType;
@Schema(example = "HORIZONTAL")
public EnumOptionData loanScheduleProcessingType;
} }
@Schema(description = "PutLoanProductsProductIdRequest") @Schema(description = "PutLoanProductsProductIdRequest")
@@ -44,6 +44,8 @@ import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
import org.apache.fineract.portfolio.floatingrates.data.FloatingRateData; import org.apache.fineract.portfolio.floatingrates.data.FloatingRateData;
import org.apache.fineract.portfolio.fund.data.FundData; import org.apache.fineract.portfolio.fund.data.FundData;
import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData; import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
@@ -173,6 +175,8 @@ public class LoanProductData implements Serializable {
private final List<EnumOptionData> advancedPaymentAllocationTransactionTypes; private final List<EnumOptionData> advancedPaymentAllocationTransactionTypes;
private final List<EnumOptionData> advancedPaymentAllocationFutureInstallmentAllocationRules; private final List<EnumOptionData> advancedPaymentAllocationFutureInstallmentAllocationRules;
private final List<EnumOptionData> advancedPaymentAllocationTypes; private final List<EnumOptionData> advancedPaymentAllocationTypes;
private final List<EnumOptionData> loanScheduleTypeOptions;
private final List<EnumOptionData> loanScheduleProcessingTypeOptions;
private final List<EnumOptionData> interestRecalculationFrequencyTypeOptions; private final List<EnumOptionData> interestRecalculationFrequencyTypeOptions;
private final List<FloatingRateData> floatingRateOptions; private final List<FloatingRateData> floatingRateOptions;
@@ -209,6 +213,9 @@ public class LoanProductData implements Serializable {
private final boolean disableScheduleExtensionForDownPayment; private final boolean disableScheduleExtensionForDownPayment;
private final boolean enableInstallmentLevelDelinquency; private final boolean enableInstallmentLevelDelinquency;
private final EnumOptionData loanScheduleType;
private final EnumOptionData loanScheduleProcessingType;
/** /**
* Used when returning lookup information about loan product for dropdowns. * Used when returning lookup information about loan product for dropdowns.
*/ */
@@ -304,6 +311,10 @@ public class LoanProductData implements Serializable {
final EnumOptionData repaymentStartDateType = null; final EnumOptionData repaymentStartDateType = null;
final boolean disableScheduleExtensionForDownPayment = false; final boolean disableScheduleExtensionForDownPayment = false;
final boolean enableInstallmentLevelDelinquency = false; final boolean enableInstallmentLevelDelinquency = false;
final EnumOptionData loanScheduleType = null;
final EnumOptionData loanScheduleProcessingType = null;
final EnumOptionData loanScheduleTypeOptions = null;
final EnumOptionData loanScheduleProcessingTypeOptions = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -323,7 +334,8 @@ public class LoanProductData implements Serializable {
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation, repaymentStartDateType, disableScheduleExtensionForDownPayment, enableInstallmentLevelDelinquency); paymentAllocation, repaymentStartDateType, disableScheduleExtensionForDownPayment, enableInstallmentLevelDelinquency,
loanScheduleType, loanScheduleProcessingType);
} }
@@ -420,6 +432,8 @@ public class LoanProductData implements Serializable {
final EnumOptionData repaymentStartDateType = null; final EnumOptionData repaymentStartDateType = null;
final boolean disableScheduleExtensionForDownPayment = false; final boolean disableScheduleExtensionForDownPayment = false;
final boolean enableInstallmentLevelDelinquency = false; final boolean enableInstallmentLevelDelinquency = false;
final EnumOptionData loanScheduleType = null;
final EnumOptionData loanScheduleProcessingType = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -439,7 +453,8 @@ public class LoanProductData implements Serializable {
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation, repaymentStartDateType, disableScheduleExtensionForDownPayment, enableInstallmentLevelDelinquency); paymentAllocation, repaymentStartDateType, disableScheduleExtensionForDownPayment, enableInstallmentLevelDelinquency,
loanScheduleType, loanScheduleProcessingType);
} }
@@ -543,6 +558,8 @@ public class LoanProductData implements Serializable {
final EnumOptionData repaymentStartDateType = LoanEnumerations.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE); final EnumOptionData repaymentStartDateType = LoanEnumerations.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE);
final boolean disableScheduleExtensionForDownPayment = false; final boolean disableScheduleExtensionForDownPayment = false;
final boolean enableInstallmentLevelDelinquency = false; final boolean enableInstallmentLevelDelinquency = false;
final EnumOptionData loanScheduleType = LoanScheduleType.CUMULATIVE.asEnumOptionData();
final EnumOptionData loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL.asEnumOptionData();
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -562,7 +579,8 @@ public class LoanProductData implements Serializable {
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation, repaymentStartDateType, disableScheduleExtensionForDownPayment, enableInstallmentLevelDelinquency); paymentAllocation, repaymentStartDateType, disableScheduleExtensionForDownPayment, enableInstallmentLevelDelinquency,
loanScheduleType, loanScheduleProcessingType);
} }
@@ -660,6 +678,8 @@ public class LoanProductData implements Serializable {
final EnumOptionData repaymentStartDateType = LoanEnumerations.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE); final EnumOptionData repaymentStartDateType = LoanEnumerations.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE);
final boolean disableScheduleExtensionForDownPayment = false; final boolean disableScheduleExtensionForDownPayment = false;
final boolean enableInstallmentLevelDelinquency = false; final boolean enableInstallmentLevelDelinquency = false;
final EnumOptionData loanScheduleType = null;
final EnumOptionData loanScheduleProcessingType = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -679,7 +699,8 @@ public class LoanProductData implements Serializable {
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation, repaymentStartDateType, disableScheduleExtensionForDownPayment, enableInstallmentLevelDelinquency); paymentAllocation, repaymentStartDateType, disableScheduleExtensionForDownPayment, enableInstallmentLevelDelinquency,
loanScheduleType, loanScheduleProcessingType);
} }
public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings, public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings,
@@ -728,7 +749,8 @@ public class LoanProductData implements Serializable {
final Integer overDueDaysForRepaymentEvent, final boolean enableDownPayment, final Integer overDueDaysForRepaymentEvent, final boolean enableDownPayment,
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment,
final Collection<AdvancedPaymentData> paymentAllocation, final EnumOptionData repaymentStartDateType, final Collection<AdvancedPaymentData> paymentAllocation, final EnumOptionData repaymentStartDateType,
final boolean disableScheduleExtensionForDownPayment, final boolean enableInstallmentLevelDelinquency) { final boolean disableScheduleExtensionForDownPayment, final boolean enableInstallmentLevelDelinquency,
final EnumOptionData loanScheduleType, final EnumOptionData loanScheduleProcessingType) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.shortName = shortName; this.shortName = shortName;
@@ -857,6 +879,10 @@ public class LoanProductData implements Serializable {
this.advancedPaymentAllocationTypes = PaymentAllocationType.getValuesAsEnumOptionDataList(); this.advancedPaymentAllocationTypes = PaymentAllocationType.getValuesAsEnumOptionDataList();
this.disableScheduleExtensionForDownPayment = disableScheduleExtensionForDownPayment; this.disableScheduleExtensionForDownPayment = disableScheduleExtensionForDownPayment;
this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency; this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency;
this.loanScheduleType = loanScheduleType;
this.loanScheduleProcessingType = loanScheduleProcessingType;
this.loanScheduleTypeOptions = null;
this.loanScheduleProcessingTypeOptions = null;
} }
public LoanProductData(final LoanProductData productData, final Collection<ChargeData> chargeOptions, public LoanProductData(final LoanProductData productData, final Collection<ChargeData> chargeOptions,
@@ -876,7 +902,8 @@ public class LoanProductData implements Serializable {
final Collection<DelinquencyBucketData> delinquencyBucketOptions, final List<EnumOptionData> repaymentStartDateTypeOptions, final Collection<DelinquencyBucketData> delinquencyBucketOptions, final List<EnumOptionData> repaymentStartDateTypeOptions,
final List<EnumOptionData> advancedPaymentAllocationTransactionTypes, final List<EnumOptionData> advancedPaymentAllocationTransactionTypes,
final List<EnumOptionData> advancedPaymentAllocationFutureInstallmentAllocationRules, final List<EnumOptionData> advancedPaymentAllocationFutureInstallmentAllocationRules,
final List<EnumOptionData> advancedPaymentAllocationTypes) { final List<EnumOptionData> advancedPaymentAllocationTypes, final List<EnumOptionData> loanScheduleTypeOptions,
final List<EnumOptionData> loanScheduleProcessingTypeOptions) {
this.id = productData.id; this.id = productData.id;
this.name = productData.name; this.name = productData.name;
this.shortName = productData.shortName; this.shortName = productData.shortName;
@@ -1022,6 +1049,10 @@ public class LoanProductData implements Serializable {
this.advancedPaymentAllocationTypes = advancedPaymentAllocationTypes; this.advancedPaymentAllocationTypes = advancedPaymentAllocationTypes;
this.disableScheduleExtensionForDownPayment = productData.disableScheduleExtensionForDownPayment; this.disableScheduleExtensionForDownPayment = productData.disableScheduleExtensionForDownPayment;
this.enableInstallmentLevelDelinquency = productData.enableInstallmentLevelDelinquency; this.enableInstallmentLevelDelinquency = productData.enableInstallmentLevelDelinquency;
this.loanScheduleType = productData.getLoanScheduleType();
this.loanScheduleProcessingType = productData.getLoanScheduleProcessingType();
this.loanScheduleProcessingTypeOptions = loanScheduleProcessingTypeOptions;
this.loanScheduleTypeOptions = loanScheduleTypeOptions;
} }
private Collection<ChargeData> nullIfEmpty(final Collection<ChargeData> charges) { private Collection<ChargeData> nullIfEmpty(final Collection<ChargeData> charges) {
@@ -37,6 +37,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams; import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.accounting.common.AccountingRuleType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
@@ -46,14 +47,19 @@ import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.domain.AdvancedPaymentAllocationsJsonParser;
import org.apache.fineract.portfolio.loanproduct.domain.AdvancedPaymentAllocationsValidator;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy; import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductValueConditionType; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductValueConditionType;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException; import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException;
@@ -165,7 +171,7 @@ public final class LoanProductDataValidator {
LoanProductConstants.ENABLE_DOWN_PAYMENT, LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, LoanProductConstants.ENABLE_DOWN_PAYMENT, LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT,
LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, LoanProductConstants.REPAYMENT_START_DATE_TYPE, LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, LoanProductConstants.REPAYMENT_START_DATE_TYPE,
LoanProductConstants.DISABLE_SCHEDULE_EXTENSION_FOR_DOWN_PAYMENT, LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, LoanProductConstants.DISABLE_SCHEDULE_EXTENSION_FOR_DOWN_PAYMENT, LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY,
LoanProductConstants.LOAN_SCHEDULE_TYPE)); LoanProductConstants.LOAN_SCHEDULE_TYPE, LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE));
private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName,
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName,
@@ -179,10 +185,12 @@ public final class LoanProductDataValidator {
public static final String DOT = "."; public static final String DOT = ".";
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final AdvancedPaymentAllocationsJsonParser advancedPaymentAllocationsJsonParser;
private final AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator;
public void validateForCreate(final String json) { public void validateForCreate(final JsonCommand command) {
String json = command.json();
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
@@ -777,6 +785,28 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_TYPE).value(loanScheduleType).ignoreIfNull() baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_TYPE).value(loanScheduleType).ignoreIfNull()
.isOneOfEnumValues(LoanScheduleType.class); .isOneOfEnumValues(LoanScheduleType.class);
String loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL.name();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) {
loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE,
element);
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType)
.ignoreIfNull().isOneOfEnumValues(LoanScheduleProcessingType.class);
if (LoanScheduleProcessingType.VERTICAL.equals(LoanScheduleProcessingType.valueOf(loanScheduleProcessingType))
&& !AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY
.equals(transactionProcessingStrategyCode)) {
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).failWithCode(
"supported.only.for.progressive.loan.schedule.handling",
"Vertical repayment schedule processing is only available with `Advanced payment allocation` strategy");
}
}
if (AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategyCode)
&& LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType)) {
advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, transactionProcessingStrategyCode));
}
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@@ -1137,7 +1167,8 @@ public final class LoanProductDataValidator {
LoanPreClosureInterestCalculationStrategy.getMinValue(), LoanPreClosureInterestCalculationStrategy.getMaxValue()); LoanPreClosureInterestCalculationStrategy.getMinValue(), LoanPreClosureInterestCalculationStrategy.getMaxValue());
} }
public void validateForUpdate(final String json, final LoanProduct loanProduct) { public void validateForUpdate(final JsonCommand command, final LoanProduct loanProduct) {
String json = command.json();
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
@@ -1247,9 +1278,9 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(REPAYMENT_FREQUENCY_TYPE).value(repaymentFrequencyType).notNull().inMinMaxRange(0, 3); baseDataValidator.reset().parameter(REPAYMENT_FREQUENCY_TYPE).value(repaymentFrequencyType).notNull().inMinMaxRange(0, 3);
} }
String transactionProcessingStrategyCode = loanProduct.getTransactionProcessingStrategyCode();
if (this.fromApiJsonHelper.parameterExists(TRANSACTION_PROCESSING_STRATEGY_CODE, element)) { if (this.fromApiJsonHelper.parameterExists(TRANSACTION_PROCESSING_STRATEGY_CODE, element)) {
final String transactionProcessingStrategyCode = this.fromApiJsonHelper.extractStringNamed(TRANSACTION_PROCESSING_STRATEGY_CODE, transactionProcessingStrategyCode = this.fromApiJsonHelper.extractStringNamed(TRANSACTION_PROCESSING_STRATEGY_CODE, element);
element);
baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).value(transactionProcessingStrategyCode).notBlank(); baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).value(transactionProcessingStrategyCode).notBlank();
// Validating whether the processor is existing // Validating whether the processor is existing
loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode); loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode);
@@ -1753,6 +1784,39 @@ public final class LoanProductDataValidator {
.value(enableInstallmentLevelDelinquency).ignoreIfNull().validateForBooleanValue(); .value(enableInstallmentLevelDelinquency).ignoreIfNull().validateForBooleanValue();
} }
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_TYPE, element)) {
final String loanScheduleType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_TYPE, element);
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_TYPE).value(loanScheduleType).ignoreIfNull()
.isOneOfEnumValues(LoanScheduleType.class);
}
String loanScheduleProcessingType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleProcessingType().name();
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, element)) {
loanScheduleProcessingType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE,
element);
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).value(loanScheduleProcessingType)
.ignoreIfNull().isOneOfEnumValues(LoanScheduleProcessingType.class);
}
List<LoanProductPaymentAllocationRule> allocationRules = loanProduct.getPaymentAllocationRules();
if (this.fromApiJsonHelper.parameterExists(ADVANCED_PAYMENT_ALLOCATIONS, element)
&& LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType)) {
allocationRules = advancedPaymentAllocationsJsonParser.assembleLoanProductPaymentAllocationRules(command,
transactionProcessingStrategyCode);
}
if (LoanScheduleProcessingType.HORIZONTAL.name().equals(loanScheduleProcessingType)
&& AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY
.equals(transactionProcessingStrategyCode)) {
advancedPaymentAllocationsValidator.checkGroupingOfAllocationRules(allocationRules);
}
if (LoanScheduleProcessingType.VERTICAL.equals(LoanScheduleProcessingType.valueOf(loanScheduleProcessingType))
&& !AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY
.equals(transactionProcessingStrategyCode)) {
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE).failWithCode(
"supported.only.for.progressive.loan.schedule.handling",
"Vertical repayment schedule processing is only available with `Advanced payment allocation` strategy");
}
throwExceptionIfValidationWarningsExist(dataValidationErrors); throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
@@ -43,6 +43,8 @@ import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService;
import org.apache.fineract.portfolio.common.service.CommonEnumerations; import org.apache.fineract.portfolio.common.service.CommonEnumerations;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData; import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData;
import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData.PaymentAllocationOrder; import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData.PaymentAllocationOrder;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData; import org.apache.fineract.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData;
@@ -260,7 +262,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
+ "lfr.is_floating_interest_rate_calculation_allowed as isFloatingInterestRateCalculationAllowed, " + "lfr.is_floating_interest_rate_calculation_allowed as isFloatingInterestRateCalculationAllowed, "
+ "lp.allow_variabe_installments as isVariableIntallmentsAllowed, " + "lvi.minimum_gap as minimumGap, " + "lp.allow_variabe_installments as isVariableIntallmentsAllowed, " + "lvi.minimum_gap as minimumGap, "
+ "lvi.maximum_gap as maximumGap, dbuc.id as delinquencyBucketId, dbuc.name as delinquencyBucketName, " + "lvi.maximum_gap as maximumGap, dbuc.id as delinquencyBucketId, dbuc.name as delinquencyBucketName, "
+ "lp.can_use_for_topup as canUseForTopup, lp.is_equal_amortization as isEqualAmortization " + "lp.can_use_for_topup as canUseForTopup, lp.is_equal_amortization as isEqualAmortization, lp.loan_schedule_type as loanScheduleType, lp.loan_schedule_processing_type as loanScheduleProcessingType "
+ " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id " + " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id "
+ " left join m_product_loan_recalculation_details lpr on lpr.product_id=lp.id " + " left join m_product_loan_recalculation_details lpr on lpr.product_id=lp.id "
+ " left join m_product_loan_guarantee_details lpg on lpg.loan_product_id=lp.id " + " left join m_product_loan_guarantee_details lpg on lpg.loan_product_id=lp.id "
@@ -505,6 +507,11 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
final DelinquencyBucketData delinquencyBucket = new DelinquencyBucketData(delinquencyBucketId, delinquencyBucketName, final DelinquencyBucketData delinquencyBucket = new DelinquencyBucketData(delinquencyBucketId, delinquencyBucketName,
new ArrayList<>()); new ArrayList<>());
final String loanScheduleTypeStr = rs.getString("loanScheduleType");
final LoanScheduleType loanScheduleType = LoanScheduleType.valueOf(loanScheduleTypeStr);
final String loanScheduleProcessingTypeStr = rs.getString("loanScheduleProcessingType");
final LoanScheduleProcessingType loanScheduleProcessingType = LoanScheduleProcessingType.valueOf(loanScheduleProcessingTypeStr);
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType,
@@ -525,7 +532,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket,
dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment,
enableAutoRepaymentForDownPayment, advancedPaymentData, repaymentStartDateType, disableScheduleExtensionForDownPayment, enableAutoRepaymentForDownPayment, advancedPaymentData, repaymentStartDateType, disableScheduleExtensionForDownPayment,
enableInstallmentLevelDelinquency); enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData());
} }
} }
@@ -97,7 +97,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
this.context.authenticatedUser(); this.context.authenticatedUser();
this.fromApiJsonDeserializer.validateForCreate(command.json()); this.fromApiJsonDeserializer.validateForCreate(command);
validateInputDates(command); validateInputDates(command);
final Fund fund = findFundByIdIfProvided(command.longValueOfParameterNamed("fundId")); final Fund fund = findFundByIdIfProvided(command.longValueOfParameterNamed("fundId"));
@@ -172,7 +172,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
final LoanProduct product = this.loanProductRepository.findById(loanProductId) final LoanProduct product = this.loanProductRepository.findById(loanProductId)
.orElseThrow(() -> new LoanProductNotFoundException(loanProductId)); .orElseThrow(() -> new LoanProductNotFoundException(loanProductId));
this.fromApiJsonDeserializer.validateForUpdate(command.json(), product); this.fromApiJsonDeserializer.validateForUpdate(command, product);
validateInputDates(command); validateInputDates(command);
if (anyChangeInCriticalFloatingRateLinkedParams(command, product) if (anyChangeInCriticalFloatingRateLinkedParams(command, product)
@@ -43,6 +43,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRul
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
@@ -182,6 +184,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Money overpaidAmount = Money.zero(currency); Money overpaidAmount = Money.zero(currency);
Money zero = Money.zero(currency); Money zero = Money.zero(currency);
Loan loan = Mockito.mock(Loan.class); Loan loan = Mockito.mock(Loan.class);
LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
Money chargeAmountMoney = Money.of(currency, chargeAmount); Money chargeAmountMoney = Money.of(currency, chargeAmount);
BigDecimal transactionAmount = BigDecimal.valueOf(120.00); BigDecimal transactionAmount = BigDecimal.valueOf(120.00);
Money transactionAmountMoney = Money.of(currency, transactionAmount); Money transactionAmountMoney = Money.of(currency, transactionAmount);
@@ -198,6 +201,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate); Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney); Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.getLoan()).thenReturn(loan); Mockito.when(loanTransaction.getLoan()).thenReturn(loan);
Mockito.when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
Mockito.when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL);
Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate); Mockito.when(loan.getDisbursementDate()).thenReturn(disbursementDate);
Mockito.when(installment.isFirstPeriod()).thenReturn(true); Mockito.when(installment.isFirstPeriod()).thenReturn(true);
Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())) Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate()))
@@ -123,7 +123,7 @@ public class DefaultScheduledDateGeneratorTest {
null, null, null, null, null, Money.of(fromApplicationCurrency(dollarCurrency), ZERO), false, null, EMPTY_LIST, null, null, null, null, null, Money.of(fromApplicationCurrency(dollarCurrency), ZERO), false, null, EMPTY_LIST,
BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO, BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO,
null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false, null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false,
DISBURSEMENT_DATE, submittedOnDate, false, CUMULATIVE); DISBURSEMENT_DATE, submittedOnDate, false, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL);
} }
private HolidayDetailDTO createHolidayDTO() { private HolidayDetailDTO createHolidayDTO() {
@@ -20,6 +20,7 @@ package org.apache.fineract.integrationtests;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.RequestSpecBuilder;
@@ -29,10 +30,13 @@ import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification; import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.BusinessDateRequest; import org.apache.fineract.client.models.BusinessDateRequest;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
@@ -47,9 +51,11 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionI
import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest; import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest;
import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.integrationtests.common.BusinessDateHelper; import org.apache.fineract.integrationtests.common.BusinessDateHelper;
import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper; import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.Utils;
@@ -60,6 +66,8 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
@@ -101,7 +109,8 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
final Account expenseAccount = accountHelper.createExpenseAccount(); final Account expenseAccount = accountHelper.createExpenseAccount();
final Account overpaymentAccount = accountHelper.createLiabilityAccount(); final Account overpaymentAccount = accountHelper.createLiabilityAccount();
commonLoanProductId = createLoanProduct("500", "15", "4", true, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); commonLoanProductId = createLoanProduct("500", "15", "4", true, LoanScheduleType.PROGRESSIVE, LoanScheduleProcessingType.HORIZONTAL,
assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
} }
@@ -1739,8 +1748,8 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
final Account incomeAccount = accountHelper.createIncomeAccount(); final Account incomeAccount = accountHelper.createIncomeAccount();
final Account expenseAccount = accountHelper.createExpenseAccount(); final Account expenseAccount = accountHelper.createExpenseAccount();
final Account overpaymentAccount = accountHelper.createLiabilityAccount(); final Account overpaymentAccount = accountHelper.createLiabilityAccount();
Integer localLoanProductId = createLoanProduct("500", "15", "4", false, assetAccount, incomeAccount, expenseAccount, Integer localLoanProductId = createLoanProduct("500", "15", "4", false, LoanScheduleType.PROGRESSIVE,
overpaymentAccount); LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, 500L, 45, 15, 3, 0, final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, 500L, 45, 15, 3, 0,
"01 January 2023", "01 January 2023"); "01 January 2023", "01 January 2023");
@@ -1824,8 +1833,8 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
final Account incomeAccount = accountHelper.createIncomeAccount(); final Account incomeAccount = accountHelper.createIncomeAccount();
final Account expenseAccount = accountHelper.createExpenseAccount(); final Account expenseAccount = accountHelper.createExpenseAccount();
final Account overpaymentAccount = accountHelper.createLiabilityAccount(); final Account overpaymentAccount = accountHelper.createLiabilityAccount();
Integer localLoanProductId = createLoanProduct("500", "15", "4", false, assetAccount, incomeAccount, expenseAccount, Integer localLoanProductId = createLoanProduct("500", "15", "4", false, LoanScheduleType.PROGRESSIVE,
overpaymentAccount); LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, 500L, 45, 15, 3, 0, final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, 500L, 45, 15, 3, 0,
"01 January 2023", "01 January 2023"); "01 January 2023", "01 January 2023");
@@ -1904,6 +1913,318 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
} }
} }
// UC103: Advanced payment allocation, add charge after maturity -> N+1 installment, repay the loan
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Disburse the loan (500)
// 2. Add charge after maturity date
// 3. Fully repay the loan
@Test
public void uc103() {
try {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
.date("2023.02.22").dateFormat("yyyy.MM.dd").locale("en"));
final Account assetAccount = accountHelper.createAssetAccount();
final Account incomeAccount = accountHelper.createIncomeAccount();
final Account expenseAccount = accountHelper.createExpenseAccount();
final Account overpaymentAccount = accountHelper.createLiabilityAccount();
Integer localLoanProductId = createLoanProduct("500", "15", "4", false, LoanScheduleType.PROGRESSIVE,
LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, 500L, 45, 15, 3, 0,
"01 January 2023", "01 January 2023");
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN)
.approvedOnDate("01 January 2023").locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN)
.transactionAmount(BigDecimal.valueOf(500.00)).locale("en"));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 1, 1), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 1, 16), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 1, 31), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 2, 15), 125.0, 0.0, 125.0, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
// Add Charge Penalty
Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "100", true));
Integer penalty1LoanChargeId = loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(),
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "22 February 2023", "100"));
assertNotNull(penalty1LoanChargeId);
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 600.0, 0.0, 500.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 1, 1), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 1, 16), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 1, 31), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 2, 15), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 2, 22), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 100.0, 0.0, 100.0, 0.0, 0.0,
0.0, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
.dateFormat(DATETIME_PATTERN).transactionDate("22 February 2023").locale("en").transactionAmount(600.0));
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 0.0, 600.0, 0.0, 500.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 1, 1), 125.0, 125.0, 0.0, 0.0, 125.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 1, 16), 125.0, 125.0, 0.0, 0.0, 125.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 1, 31), 125.0, 125.0, 0.0, 0.0, 125.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 2, 15), 125.0, 125.0, 0.0, 0.0, 125.0);
validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 2, 22), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 100.0, 100.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getClosedObligationsMet());
} finally {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
}
}
// UC104: Advanced payment allocation, horizontal repayment processing
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Disburse the loan (500)
// 2. Add charge for 3rd and 4th installments
// 3. Pay little more than the charge of 1st installment
// 4. Pay little more than the principal of 1st installment and charge of 2nd installment
@Test
public void uc104() {
try {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
.date("2023.02.22").dateFormat("yyyy.MM.dd").locale("en"));
final Account assetAccount = accountHelper.createAssetAccount();
final Account incomeAccount = accountHelper.createIncomeAccount();
final Account expenseAccount = accountHelper.createExpenseAccount();
final Account overpaymentAccount = accountHelper.createLiabilityAccount();
Integer localLoanProductId = createLoanProduct("500", "15", "4", false, LoanScheduleType.PROGRESSIVE,
LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, 500L, 45, 15, 3, 0,
"01 January 2023", "01 January 2023");
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN)
.approvedOnDate("01 January 2023").locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN)
.transactionAmount(BigDecimal.valueOf(500.00)).locale("en"));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 1, 1), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 1, 16), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 1, 31), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 2, 15), 125.0, 0.0, 125.0, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
// Add Charge Penalty
Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "25", true));
loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(),
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "20 January 2023", "25"));
loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(),
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "10 February 2023", "25"));
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 550.0, 0.0, 500.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 1, 1), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 1, 16), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 1, 31), 125.0, 0.0, 125.0, 0.0, 0.0, 0.0, 25.0, 0.0, 25.0, 0.0, 0.0,
0.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 2, 15), 125.0, 0.0, 125.0, 0.0, 0.0, 0.0, 25.0, 0.0, 25.0, 0.0, 0.0,
0.0, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
.dateFormat(DATETIME_PATTERN).transactionDate("22 February 2023").locale("en").transactionAmount(260.0));
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 290.0, 260.0, 250.0, 250.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 1, 1), 125.0, 125.0, 0.0, 0.0, 125.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 1, 16), 125.0, 125.0, 0.0, 0.0, 125.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 1, 31), 125.0, 0.0, 125.0, 0.0, 0.0, 0.0, 25.0, 10.0, 15.0, 0.0, 0.0,
0.0, 0.0, 10.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 2, 15), 125.0, 0.0, 125.0, 0.0, 0.0, 0.0, 25.0, 0.0, 25.0, 0.0, 0.0,
0.0, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
.dateFormat(DATETIME_PATTERN).transactionDate("22 February 2023").locale("en").transactionAmount(26.0));
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 264.0, 286.0, 239.0, 261.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 1, 1), 125.0, 125.0, 0.0, 0.0, 125.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 1, 16), 125.0, 125.0, 0.0, 0.0, 125.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 1, 31), 125.0, 11.0, 114.0, 0.0, 0.0, 0.0, 25.0, 25.0, 0.0, 0.0, 0.0,
0.0, 0.0, 36.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 2, 15), 125.0, 0.0, 125.0, 0.0, 0.0, 0.0, 25.0, 0.0, 25.0, 0.0, 0.0,
0.0, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
} finally {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
}
}
// UC105: Advanced payment allocation, vertical repayment processing
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Disburse the loan (500)
// 2. Add charge for 3rd and 4th installments
// 3. Pay little more than the charge of 1st installment
// 4. Pay little more than the charges
@Test
public void uc105() {
try {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
.date("2023.02.22").dateFormat("yyyy.MM.dd").locale("en"));
final Account assetAccount = accountHelper.createAssetAccount();
final Account incomeAccount = accountHelper.createIncomeAccount();
final Account expenseAccount = accountHelper.createExpenseAccount();
final Account overpaymentAccount = accountHelper.createLiabilityAccount();
Integer localLoanProductId = createLoanProduct("500", "15", "4", false, LoanScheduleType.PROGRESSIVE,
LoanScheduleProcessingType.VERTICAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId, 500L, 45, 15, 3, 0,
"01 January 2023", "01 January 2023");
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN)
.approvedOnDate("01 January 2023").locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN)
.transactionAmount(BigDecimal.valueOf(500.00)).locale("en"));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 1, 1), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 1, 16), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 1, 31), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 2, 15), 125.0, 0.0, 125.0, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
// Add Charge Penalty
Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "25", true));
loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(),
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "20 January 2023", "25"));
loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(),
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "10 February 2023", "25"));
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 550.0, 0.0, 500.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 1, 1), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 1, 16), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 1, 31), 125.0, 0.0, 125.0, 0.0, 0.0, 0.0, 25.0, 0.0, 25.0, 0.0, 0.0,
0.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 2, 15), 125.0, 0.0, 125.0, 0.0, 0.0, 0.0, 25.0, 0.0, 25.0, 0.0, 0.0,
0.0, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
.dateFormat(DATETIME_PATTERN).transactionDate("22 February 2023").locale("en").transactionAmount(26.0));
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 524.0, 26.0, 500.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 1, 1), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 1, 16), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 1, 31), 125.0, 0.0, 125.0, 0.0, 0.0, 0.0, 25.0, 25.0, 0.0, 0.0, 0.0,
0.0, 0.0, 25.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 2, 15), 125.0, 0.0, 125.0, 0.0, 0.0, 0.0, 25.0, 1.0, 24.0, 0.0, 0.0,
0.0, 0.0, 1.0);
assertTrue(loanDetails.getStatus().getActive());
loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
.dateFormat(DATETIME_PATTERN).transactionDate("22 February 2023").locale("en").transactionAmount(26.0));
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 498.0, 52.0, 498.0, 2.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 1, 1), 125.0, 2.0, 123.0, 0.0, 2.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 1, 16), 125.0, 0.0, 125.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 1, 31), 125.0, 0.0, 125.0, 0.0, 0.0, 0.0, 25.0, 25.0, 0.0, 0.0, 0.0,
0.0, 0.0, 25.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 2, 15), 125.0, 0.0, 125.0, 0.0, 0.0, 0.0, 25.0, 25.0, 0.0, 0.0, 0.0,
0.0, 0.0, 25.0);
assertTrue(loanDetails.getStatus().getActive());
} finally {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
}
}
// UC106: Advanced payment allocation, horizontal repayment processing, mixed grouping of allocation rules
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Create a Loan product, but the allocation rules are mixed -> expect validation error
@Test
public void uc106() {
try {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
.date("2023.02.22").dateFormat("yyyy.MM.dd").locale("en"));
final Account assetAccount = accountHelper.createAssetAccount();
final Account incomeAccount = accountHelper.createIncomeAccount();
final Account expenseAccount = accountHelper.createExpenseAccount();
final Account overpaymentAccount = accountHelper.createLiabilityAccount();
AdvancedPaymentData defaultPaymentAllocation = createDefaultPaymentAllocationWithMixedGrouping();
ArrayList<HashMap<String, Object>> loanProductErrorData = createLoanProductGetError("500", "15", "4", false,
LoanScheduleType.PROGRESSIVE, LoanScheduleProcessingType.HORIZONTAL, defaultPaymentAllocation, assetAccount,
incomeAccount, expenseAccount, overpaymentAccount);
assertNotNull(loanProductErrorData);
assertEquals("mixed.due.type.allocation.rules.are.not.supported.with.horizontal.installment.processing",
loanProductErrorData.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
} finally {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
}
}
// UC107: Advanced payment allocation, vertical repayment processing, mixed grouping of allocation rules
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Create a Loan product, but the allocation rules are mixed -> no validation error
// 2. Change transaction processor while submitting loan -> expect validation error
// 3. Change to HORIZONTAL loan schedule processing and allocation rules are mixed - > expect validation error
@Test
public void uc107() {
try {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
.date("2023.02.22").dateFormat("yyyy.MM.dd").locale("en"));
final Account assetAccount = accountHelper.createAssetAccount();
final Account incomeAccount = accountHelper.createIncomeAccount();
final Account expenseAccount = accountHelper.createExpenseAccount();
final Account overpaymentAccount = accountHelper.createLiabilityAccount();
AdvancedPaymentData defaultPaymentAllocation = createDefaultPaymentAllocationWithMixedGrouping();
Integer localLoanProductId = createLoanProduct("500", "15", "4", false, LoanScheduleType.PROGRESSIVE,
LoanScheduleProcessingType.VERTICAL, defaultPaymentAllocation, assetAccount, incomeAccount, expenseAccount,
overpaymentAccount);
assertNotNull(localLoanProductId);
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
() -> applyForLoanApplication(client.getClientId(), localLoanProductId, 500L, 45, 15, 3, 0, "01 January 2023",
"01 January 2023", FineractStyleLoanRepaymentScheduleTransactionProcessor.STRATEGY_CODE,
LoanScheduleType.PROGRESSIVE.name(), LoanScheduleProcessingType.VERTICAL.name()));
assertEquals(400, exception.getResponse().code());
assertTrue(exception.getMessage().contains("supported.only.with.advanced.payment.allocation.strategy"));
exception = assertThrows(CallFailedRuntimeException.class,
() -> applyForLoanApplication(client.getClientId(), localLoanProductId, 500L, 45, 15, 3, 0, "01 January 2023",
"01 January 2023", AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY,
LoanScheduleType.PROGRESSIVE.name(), LoanScheduleProcessingType.HORIZONTAL.name()));
assertEquals(400, exception.getResponse().code());
assertTrue(exception.getMessage()
.contains("mixed.due.type.allocation.rules.are.not.supported.with.horizontal.installment.processing"));
} finally {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
}
}
private static void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, Double totalOutstanding, Double totalRepayment, private static void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, Double totalOutstanding, Double totalRepayment,
Double principalOutstanding, Double principalPaid, Double totalOverpaid) { Double principalOutstanding, Double principalPaid, Double totalOverpaid) {
assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding()); assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding());
@@ -1920,7 +2241,22 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
paymentAllocationOrder.setPaymentAllocationRule(pat.name()); paymentAllocationOrder.setPaymentAllocationRule(pat.name());
paymentAllocationOrder.setOrder(integer.getAndIncrement()); paymentAllocationOrder.setOrder(integer.getAndIncrement());
return paymentAllocationOrder; return paymentAllocationOrder;
}).toList(); }).collect(Collectors.toList());
}
private static AdvancedPaymentData createDefaultPaymentAllocationWithMixedGrouping() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("DEFAULT");
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.DUE_PENALTY,
PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST,
PaymentAllocationType.PAST_DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
} }
private static AdvancedPaymentData createDefaultPaymentAllocation() { private static AdvancedPaymentData createDefaultPaymentAllocation() {
@@ -1954,7 +2290,44 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
} }
private static Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments, private static Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments,
boolean autoPayForDownPayment, final Account... accounts) { boolean autoPayForDownPayment, LoanScheduleType loanScheduleType, LoanScheduleProcessingType loanScheduleProcessingType,
AdvancedPaymentData allocationRuleData, final Account... accounts) {
LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
final String loanProductJSON = new LoanProductTestBuilder().withMinPrincipal(principal).withPrincipal(principal)
.withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments)
.withEnableDownPayment(true, "25", autoPayForDownPayment).withinterestRatePerPeriod("0")
.withInterestRateFrequencyTypeAsMonths()
.withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
.withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts)
.addAdvancedPaymentAllocation(allocationRuleData).withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withInterestTypeAsDecliningBalance().withMultiDisburse().withDisallowExpectedDisbursements(true)
.withLoanScheduleType(loanScheduleType).withLoanScheduleProcessingType(loanScheduleProcessingType).withDaysInMonth("30")
.withDaysInYear("365").withMoratorium("0", "0").build(null);
return loanTransactionHelper.getLoanProductId(loanProductJSON);
}
private static ArrayList<HashMap<String, Object>> createLoanProductGetError(final String principal, final String repaymentAfterEvery,
final String numberOfRepayments, boolean autoPayForDownPayment, LoanScheduleType loanScheduleType,
LoanScheduleProcessingType loanScheduleProcessingType, AdvancedPaymentData allocationRuleData, final Account... accounts) {
LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
final String loanProductJSON = new LoanProductTestBuilder().withMinPrincipal(principal).withPrincipal(principal)
.withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments)
.withEnableDownPayment(true, "25", autoPayForDownPayment).withinterestRatePerPeriod("0")
.withInterestRateFrequencyTypeAsMonths()
.withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
.withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts)
.addAdvancedPaymentAllocation(allocationRuleData).withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withInterestTypeAsDecliningBalance().withMultiDisburse().withDisallowExpectedDisbursements(true)
.withLoanScheduleType(loanScheduleType).withLoanScheduleProcessingType(loanScheduleProcessingType).withDaysInMonth("30")
.withDaysInYear("365").withMoratorium("0", "0").build(null);
LoanTransactionHelper loanTransactionHelperBadRequest = new LoanTransactionHelper(requestSpec,
new ResponseSpecBuilder().expectStatusCode(400).build());
return loanTransactionHelperBadRequest.getLoanProductError(loanProductJSON, CommonConstants.RESPONSE_ERROR);
}
private static Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments,
boolean autoPayForDownPayment, LoanScheduleType loanScheduleType, LoanScheduleProcessingType loanScheduleProcessingType,
final Account... accounts) {
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
AdvancedPaymentData goodwillCreditAllocation = createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"); AdvancedPaymentData goodwillCreditAllocation = createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT");
AdvancedPaymentData merchantIssuedRefundAllocation = createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"); AdvancedPaymentData merchantIssuedRefundAllocation = createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION");
@@ -1969,8 +2342,9 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
.addAdvancedPaymentAllocation(defaultAllocation, goodwillCreditAllocation, merchantIssuedRefundAllocation, .addAdvancedPaymentAllocation(defaultAllocation, goodwillCreditAllocation, merchantIssuedRefundAllocation,
payoutRefundAllocation) payoutRefundAllocation)
.withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withInterestTypeAsDecliningBalance().withMultiDisburse() .withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withInterestTypeAsDecliningBalance().withMultiDisburse()
.withDisallowExpectedDisbursements(true).withLoanScheduleType(LoanScheduleType.PROGRESSIVE).withDaysInMonth("30") .withDisallowExpectedDisbursements(true).withLoanScheduleType(loanScheduleType)
.withDaysInYear("365").withMoratorium("0", "0").build(null); .withLoanScheduleProcessingType(loanScheduleProcessingType).withDaysInMonth("30").withDaysInYear("365")
.withMoratorium("0", "0").build(null);
return loanTransactionHelper.getLoanProductId(loanProductJSON); return loanTransactionHelper.getLoanProductId(loanProductJSON);
} }
@@ -2022,16 +2396,27 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final Long principal, private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final Long principal,
final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final int interestRate, final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final int interestRate,
final String expectedDisbursementDate, final String submittedOnDate) { final String expectedDisbursementDate, final String submittedOnDate, String transactionProcessorCode, String loanScheduleType,
String loanScheduleProcessingType) {
LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
return loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(clientId).productId(loanProductId.longValue()) return loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(clientId).productId(loanProductId.longValue())
.expectedDisbursementDate(expectedDisbursementDate).dateFormat(DATETIME_PATTERN) .expectedDisbursementDate(expectedDisbursementDate).dateFormat(DATETIME_PATTERN)
.transactionProcessingStrategyCode(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) .transactionProcessingStrategyCode(transactionProcessorCode).locale("en").submittedOnDate(submittedOnDate)
.locale("en").submittedOnDate(submittedOnDate).amortizationType(1).interestRatePerPeriod(interestRate) .amortizationType(1).interestRatePerPeriod(interestRate).interestCalculationPeriodType(1).interestType(0)
.interestCalculationPeriodType(1).interestType(0).repaymentFrequencyType(0).repaymentEvery(repaymentAfterEvery) .repaymentFrequencyType(0).repaymentEvery(repaymentAfterEvery).repaymentFrequencyType(0)
.repaymentFrequencyType(0).numberOfRepayments(numberOfRepayments).loanTermFrequency(loanTermFrequency) .numberOfRepayments(numberOfRepayments).loanTermFrequency(loanTermFrequency).loanTermFrequencyType(0)
.loanTermFrequencyType(0).principal(BigDecimal.valueOf(principal)).loanType("individual") .principal(BigDecimal.valueOf(principal)).loanType("individual").loanScheduleProcessingType(loanScheduleProcessingType)
.maxOutstandingLoanBalance(BigDecimal.valueOf(35000))); .loanScheduleType(loanScheduleType).maxOutstandingLoanBalance(BigDecimal.valueOf(35000)));
}
private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final Long principal,
final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final int interestRate,
final String expectedDisbursementDate, final String submittedOnDate) {
LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
return applyForLoanApplication(clientId, loanProductId, principal, loanTermFrequency, repaymentAfterEvery, numberOfRepayments,
interestRate, expectedDisbursementDate, submittedOnDate,
AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY, LoanScheduleType.PROGRESSIVE.name(),
LoanScheduleProcessingType.HORIZONTAL.name());
} }
private static void validateLoanTransaction(GetLoansLoanIdResponse loanDetails, int index, double transactionAmount, private static void validateLoanTransaction(GetLoansLoanIdResponse loanDetails, int index, double transactionAmount,
@@ -54,6 +54,8 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -294,6 +296,7 @@ public class RefundForActiveLoansWithAdvancedPaymentAllocationTest {
.withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments) .withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments)
.withEnableDownPayment(true, "25", true).withinterestRatePerPeriod("0").withInterestRateFrequencyTypeAsMonths() .withEnableDownPayment(true, "25", true).withinterestRatePerPeriod("0").withInterestRateFrequencyTypeAsMonths()
.withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) .withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
.withLoanScheduleType(LoanScheduleType.PROGRESSIVE).withLoanScheduleProcessingType(LoanScheduleProcessingType.VERTICAL)
.withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts) .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts)
.addAdvancedPaymentAllocation(defaultAllocation).withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0") .addAdvancedPaymentAllocation(defaultAllocation).withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0")
.build(null); .build(null);
@@ -28,6 +28,7 @@ import java.util.Map;
import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.accounting.Account; import org.apache.fineract.integrationtests.common.accounting.Account;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
public class LoanProductTestBuilder { public class LoanProductTestBuilder {
@@ -152,7 +153,8 @@ public class LoanProductTestBuilder {
private boolean enableAutoRepaymentForDownPayment = false; private boolean enableAutoRepaymentForDownPayment = false;
private Integer repaymentStartDateType = null; private Integer repaymentStartDateType = null;
private boolean disableScheduleExtensionForDownPayment = false; private boolean disableScheduleExtensionForDownPayment = false;
private LoanScheduleType loanScheduleType = LoanScheduleType.CUMULATIVE; private String loanScheduleType = LoanScheduleType.CUMULATIVE.name();
private String loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL.name();
public String build() { public String build() {
final HashMap<String, Object> map = build(null, null); final HashMap<String, Object> map = build(null, null);
@@ -310,6 +312,7 @@ public class LoanProductTestBuilder {
map.put("disableScheduleExtensionForDownPayment", disableScheduleExtensionForDownPayment); map.put("disableScheduleExtensionForDownPayment", disableScheduleExtensionForDownPayment);
} }
map.put("loanScheduleType", loanScheduleType); map.put("loanScheduleType", loanScheduleType);
map.put("loanScheduleProcessingType", loanScheduleProcessingType);
return map; return map;
} }
@@ -751,7 +754,12 @@ public class LoanProductTestBuilder {
} }
public LoanProductTestBuilder withLoanScheduleType(LoanScheduleType loanScheduleType) { public LoanProductTestBuilder withLoanScheduleType(LoanScheduleType loanScheduleType) {
this.loanScheduleType = loanScheduleType; this.loanScheduleType = loanScheduleType.name();
return this;
}
public LoanProductTestBuilder withLoanScheduleProcessingType(LoanScheduleProcessingType loanScheduleProcessingType) {
this.loanScheduleProcessingType = loanScheduleProcessingType.name();
return this; return this;
} }
} }