FINERACT-1968: Support horizontal and vertical installment processing
This commit is contained in:
+2
-2
@@ -24,9 +24,9 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
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
|
||||
public String getCode() {
|
||||
|
||||
+23
@@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
|
||||
|
||||
@Entity
|
||||
@@ -424,6 +425,24 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
||||
this.penaltyCharges = null;
|
||||
}
|
||||
|
||||
public interface PaymentFunction {
|
||||
|
||||
Money accept(LocalDate transactionDate, Money transactionAmountRemaining);
|
||||
}
|
||||
|
||||
public PaymentFunction getPaymentFunction(AllocationType allocationType, PaymentAction action) {
|
||||
return switch (allocationType) {
|
||||
case PENALTY -> PaymentAction.PAY.equals(action) ? this::payPenaltyChargesComponent
|
||||
: PaymentAction.UNPAY.equals(action) ? this::unpayPenaltyChargesComponent : null;
|
||||
case FEE -> PaymentAction.PAY.equals(action) ? this::payFeeChargesComponent
|
||||
: PaymentAction.UNPAY.equals(action) ? this::unpayFeeChargesComponent : null;
|
||||
case INTEREST -> PaymentAction.PAY.equals(action) ? this::payInterestComponent
|
||||
: PaymentAction.UNPAY.equals(action) ? this::unpayInterestComponent : null;
|
||||
case PRINCIPAL -> PaymentAction.PAY.equals(action) ? this::payPrincipalComponent
|
||||
: PaymentAction.UNPAY.equals(action) ? this::unpayPrincipalComponent : null;
|
||||
};
|
||||
}
|
||||
|
||||
public Money payPenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
|
||||
|
||||
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
|
||||
@@ -967,4 +986,8 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
||||
public void resetPrincipalDue() {
|
||||
this.principal = null;
|
||||
}
|
||||
|
||||
public enum PaymentAction {
|
||||
PAY, UNPAY
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -765,7 +765,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
||||
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) {
|
||||
|
||||
Money amountRemaining = feeCharges;
|
||||
+309
-175
@@ -18,15 +18,21 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import static java.util.stream.Collectors.mapping;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
@@ -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.SingleLoanChargeRepaymentScheduleProcessingWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.DueType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
|
||||
@@ -58,7 +66,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
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
|
||||
public String getCode() {
|
||||
@@ -104,6 +112,113 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
|
||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
if (charges != null) {
|
||||
for (final LoanCharge loanCharge : charges) {
|
||||
if (!loanCharge.isDueAtDisbursement()) {
|
||||
loanCharge.resetPaidAmount(currency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||
currentInstallment.resetBalances();
|
||||
currentInstallment.updateDerivedFields(currency, disbursementDate);
|
||||
}
|
||||
|
||||
List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges);
|
||||
|
||||
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
|
||||
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
|
||||
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency,
|
||||
installments, charges, changedTransactionDetail));
|
||||
chargeOrTransaction.getLoanCharge()
|
||||
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
|
||||
}
|
||||
reprocessInstallments(installments, currency);
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, Money overpaidAmount) {
|
||||
switch (loanTransaction.getTypeOf()) {
|
||||
case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments);
|
||||
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
|
||||
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
|
||||
case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments);
|
||||
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
|
||||
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
|
||||
handleRepayment(loanTransaction, currency, installments, charges);
|
||||
case CHARGE_OFF -> handleChargeOff(loanTransaction, currency, installments);
|
||||
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, currency, installments, charges);
|
||||
// TODO: Cover rest of the transaction types
|
||||
default -> {
|
||||
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
Money zero = Money.zero(currency);
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
||||
|
||||
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
|
||||
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
|
||||
LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
|
||||
.orElse(defaultPaymentAllocationRule);
|
||||
Balances balances = new Balances(zero, zero, zero, zero);
|
||||
List<PaymentAllocationType> paymentAllocationTypes;
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule;
|
||||
if (PaymentAllocationTransactionType.DEFAULT.equals(paymentAllocationRule.getTransactionType())) {
|
||||
// if the allocation rule is not defined then the reverse order of the default allocation rule will be used
|
||||
paymentAllocationTypes = new ArrayList<>(paymentAllocationRule.getAllocationTypes());
|
||||
Collections.reverse(paymentAllocationTypes);
|
||||
futureInstallmentAllocationRule = FutureInstallmentAllocationRule.LAST_INSTALLMENT;
|
||||
} else {
|
||||
paymentAllocationTypes = paymentAllocationRule.getAllocationTypes();
|
||||
futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
|
||||
}
|
||||
if (LoanScheduleProcessingType.HORIZONTAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
// TODO: implementation
|
||||
} else if (LoanScheduleProcessingType.VERTICAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
|
||||
transactionAmountUnprocessed = refundTransaction(loanTransaction, currency, installments, zero, transactionMappings,
|
||||
transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, paymentAllocationType);
|
||||
if (!transactionAmountUnprocessed.isGreaterThanZero()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
|
||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
|
||||
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
|
||||
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
|
||||
Integer installmentNumber = null;
|
||||
|
||||
final Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
|
||||
if (feeCharges.isGreaterThanZero()) {
|
||||
undoChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, installmentNumber);
|
||||
}
|
||||
|
||||
final Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
|
||||
if (penaltyCharges.isGreaterThanZero()) {
|
||||
undoChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, installmentNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges,
|
||||
ChangedTransactionDetail changedTransactionDetail) {
|
||||
@@ -139,37 +254,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
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
|
||||
private List<ChargeOrTransaction> createSortedChargesAndTransactionsList(List<LoanTransaction> loanTransactions,
|
||||
Set<LoanCharge> charges) {
|
||||
@@ -184,26 +268,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
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,
|
||||
List<LoanRepaymentScheduleInstallment> installments) {
|
||||
updateLoanSchedule(loanTransaction, currency, installments);
|
||||
@@ -271,68 +335,44 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return loanTransactionToRepaymentScheduleMapping;
|
||||
}
|
||||
|
||||
private Money payAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment,
|
||||
private Money processPaymentAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment,
|
||||
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction action) {
|
||||
LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
Money zero = transactionAmountUnprocessed.zero();
|
||||
return switch (paymentAllocationType.getAllocationType()) {
|
||||
case PENALTY -> {
|
||||
Money subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(subPenaltyPortion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, subPenaltyPortion);
|
||||
yield subPenaltyPortion;
|
||||
LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment
|
||||
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
|
||||
Money portion = paymentFunction.accept(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(portion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, portion);
|
||||
yield portion;
|
||||
}
|
||||
case FEE -> {
|
||||
Money subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(subFeePortion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, subFeePortion, zero);
|
||||
yield subFeePortion;
|
||||
LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
|
||||
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
|
||||
Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(portion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, portion, zero);
|
||||
yield portion;
|
||||
}
|
||||
case INTEREST -> {
|
||||
Money subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(subInterestPortion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, subInterestPortion, zero, zero);
|
||||
yield subInterestPortion;
|
||||
LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
|
||||
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
|
||||
Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(portion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, portion, zero, zero);
|
||||
yield portion;
|
||||
}
|
||||
case PRINCIPAL -> {
|
||||
Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(subPrincipalPortion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, subPrincipalPortion, zero, zero, zero);
|
||||
yield subPrincipalPortion;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
LoanRepaymentScheduleInstallment.PaymentFunction functional = currentInstallment
|
||||
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
|
||||
Money portion = functional.accept(transactionDate, transactionAmountUnprocessed);
|
||||
balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(portion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, portion, zero, zero, zero);
|
||||
yield portion;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money zero,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Money transactionAmountUnprocessed,
|
||||
@@ -490,8 +487,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
if (currentInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
refundedPortion = unpayAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances);
|
||||
refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
||||
}
|
||||
}
|
||||
@@ -502,8 +500,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
if (currentInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
refundedPortion = unpayAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances);
|
||||
refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
||||
}
|
||||
}
|
||||
@@ -536,8 +535,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
Money internalUnpaidPortion = unpayAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
evenPortion, loanTransactionToRepaymentScheduleMapping, balances);
|
||||
Money internalUnpaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment,
|
||||
loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
||||
if (internalUnpaidPortion.isGreaterThanZero()) {
|
||||
refundedPortion = internalUnpaidPortion;
|
||||
}
|
||||
@@ -564,33 +564,163 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
|
||||
.orElse(defaultPaymentAllocationRule);
|
||||
Balances balances = new Balances(zero, zero, zero, zero);
|
||||
|
||||
if (LoanScheduleProcessingType.HORIZONTAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, currency, installments, transactionAmountUnprocessed,
|
||||
paymentAllocationRule, transactionMappings, balances);
|
||||
} else if (LoanScheduleProcessingType.VERTICAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, currency, installments, transactionAmountUnprocessed,
|
||||
paymentAllocationRule, transactionMappings, balances);
|
||||
}
|
||||
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
|
||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
|
||||
payAdditionalCharges(loanTransaction, currency, charges);
|
||||
|
||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction);
|
||||
}
|
||||
|
||||
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
|
||||
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Balances balances) {
|
||||
LinkedHashMap<DueType, List<PaymentAllocationType>> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream()
|
||||
.collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new,
|
||||
mapping(Function.identity(), toList())));
|
||||
|
||||
for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
|
||||
transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, currency, installments,
|
||||
transactionAmountUnprocessed, paymentAllocationsEntry.getValue(),
|
||||
paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, balances);
|
||||
}
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
private Money processAllocationsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
|
||||
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances) {
|
||||
Money paidPortion;
|
||||
boolean exit = false;
|
||||
do {
|
||||
LoanRepaymentScheduleInstallment oldestPastDueInstallment = installments.stream()
|
||||
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isAfter(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||
LoanRepaymentScheduleInstallment dueInstallment = installments.stream()
|
||||
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isOn(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||
|
||||
// For having similar logic we are populating installment list even when the future installment
|
||||
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
|
||||
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new ArrayList<>();
|
||||
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
|
||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())).toList();
|
||||
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
}
|
||||
|
||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
|
||||
switch (paymentAllocationType.getDueType()) {
|
||||
case PAST_DUE -> {
|
||||
if (oldestPastDueInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, oldestPastDueInstallment, currency);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
} else {
|
||||
exit = true;
|
||||
}
|
||||
}
|
||||
case DUE -> {
|
||||
if (dueInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, dueInstallment, currency);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
} else {
|
||||
exit = true;
|
||||
}
|
||||
}
|
||||
case IN_ADVANCE -> {
|
||||
int numberOfInstallments = inAdvanceInstallments.size();
|
||||
if (numberOfInstallments > 0) {
|
||||
// This will be the same amount as transactionAmountUnprocessed in case of the future
|
||||
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
|
||||
// Adjustment might be needed due to the divide operation and the rounding mode
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
|
||||
// Adjust the portion for the last installment
|
||||
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
|
||||
evenPortion = evenPortion.add(balanceAdjustment);
|
||||
}
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, inAdvanceInstallment, currency);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
|
||||
evenPortion, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
}
|
||||
} else {
|
||||
exit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// We are allocating till there is no pending installment or there is no more unprocessed transaction amount
|
||||
// or there is no more outstanding balance of the allocation type
|
||||
while (!exit && installments.stream().anyMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
&& transactionAmountUnprocessed.isGreaterThanZero());
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
private Money processPeriodsVertically(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
|
||||
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Balances balances) {
|
||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) {
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
|
||||
LoanRepaymentScheduleInstallment currentInstallment = null;
|
||||
Money paidPortion = zero;
|
||||
Money paidPortion = Money.zero(currency);
|
||||
do {
|
||||
Predicate<LoanRepaymentScheduleInstallment> predicate = getFilterPredicate(paymentAllocationType, currency);
|
||||
switch (paymentAllocationType.getDueType()) {
|
||||
case PAST_DUE -> {
|
||||
currentInstallment = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
.filter(e -> loanTransaction.isAfter(e.getDueDate()))
|
||||
currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isAfter(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||
if (currentInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
paidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
}
|
||||
}
|
||||
case DUE -> {
|
||||
currentInstallment = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
.filter(e -> loanTransaction.isOn(e.getDueDate()))
|
||||
currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isOn(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||
if (currentInstallment != null) {
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
paidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
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.
|
||||
List<LoanRepaymentScheduleInstallment> currentInstallments = new ArrayList<>();
|
||||
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
|
||||
currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
currentInstallments = installments.stream().filter(predicate)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())).toList();
|
||||
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
currentInstallments = installments.stream().filter(predicate)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||
currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||
currentInstallments = installments.stream().filter(predicate)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||
}
|
||||
int numberOfInstallments = currentInstallments.size();
|
||||
paidPortion = zero;
|
||||
paidPortion = Money.zero(currency);
|
||||
if (numberOfInstallments > 0) {
|
||||
// This will be the same amount as transactionAmountUnprocessed in case of the future
|
||||
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
|
||||
@@ -626,8 +756,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
Money internalPaidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
evenPortion, loanTransactionToRepaymentScheduleMapping, balances);
|
||||
Money internalPaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment,
|
||||
loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
// Some extra logic to allocate as much as possible across the installments if the
|
||||
// outstanding balances are different
|
||||
if (internalPaidPortion.isGreaterThanZero()) {
|
||||
@@ -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
|
||||
// or there is no more outstanding balance of the allocation type
|
||||
while (currentInstallment != null && transactionAmountUnprocessed.isGreaterThanZero() && paidPortion.isGreaterThanZero());
|
||||
|
||||
}
|
||||
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
|
||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
payAdditionalCharges(loanTransaction, currency, charges);
|
||||
|
||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction);
|
||||
private Predicate<LoanRepaymentScheduleInstallment> getFilterPredicate(PaymentAllocationType paymentAllocationType,
|
||||
MonetaryCurrency currency) {
|
||||
return switch (paymentAllocationType.getAllocationType()) {
|
||||
case PENALTY -> (p) -> p.getPenaltyChargesOutstanding(currency).isGreaterThanZero();
|
||||
case FEE -> (p) -> p.getFeeChargesOutstanding(currency).isGreaterThanZero();
|
||||
case INTEREST -> (p) -> p.getInterestOutstanding(currency).isGreaterThanZero();
|
||||
case PRINCIPAL -> (p) -> p.getPrincipalOutstanding(currency).isGreaterThanZero();
|
||||
};
|
||||
}
|
||||
|
||||
private void payAdditionalCharges(LoanTransaction loanTransaction, MonetaryCurrency currency, Set<LoanCharge> charges) {
|
||||
+2
-2
@@ -42,9 +42,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
|
||||
*/
|
||||
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
|
||||
public String getCode() {
|
||||
+2
-2
@@ -43,9 +43,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
|
||||
public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor
|
||||
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
|
||||
public String getCode() {
|
||||
+2
-2
@@ -43,9 +43,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
|
||||
public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor
|
||||
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
|
||||
public String getCode() {
|
||||
+1
-1
@@ -38,7 +38,7 @@ public class EarlyPaymentLoanRepaymentScheduleTransactionProcessor extends Abstr
|
||||
|
||||
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
|
||||
public String getCode() {
|
||||
+2
-2
@@ -42,9 +42,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
|
||||
@SuppressWarnings("unused")
|
||||
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
|
||||
public String getCode() {
|
||||
+2
-2
@@ -44,9 +44,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
|
||||
@SuppressWarnings("unused")
|
||||
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
|
||||
public String getCode() {
|
||||
+2
-2
@@ -37,9 +37,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
|
||||
public class InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor
|
||||
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
|
||||
public String getCode() {
|
||||
+2
-2
@@ -37,9 +37,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
|
||||
public class PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor
|
||||
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
|
||||
public String getCode() {
|
||||
+2
-2
@@ -46,9 +46,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
|
||||
*/
|
||||
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
|
||||
public String getCode() {
|
||||
+9
-5
@@ -219,6 +219,7 @@ public final class LoanApplicationTerms {
|
||||
private boolean isScheduleExtensionForDownPaymentDisabled;
|
||||
private Money disbursedPrincipal;
|
||||
private final LoanScheduleType loanScheduleType;
|
||||
private final LoanScheduleProcessingType loanScheduleProcessingType;
|
||||
|
||||
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency,
|
||||
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
|
||||
@@ -246,7 +247,7 @@ public final class LoanApplicationTerms {
|
||||
final Boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
|
||||
final Boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType,
|
||||
final LocalDate submittedOnDate, final Boolean isScheduleExtensionForDownPaymentDisabled,
|
||||
final LoanScheduleType loanScheduleType) {
|
||||
final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
|
||||
|
||||
final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
|
||||
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
|
||||
@@ -265,7 +266,7 @@ public final class LoanApplicationTerms {
|
||||
isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
|
||||
isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment,
|
||||
isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled,
|
||||
loanScheduleType);
|
||||
loanScheduleType, loanScheduleProcessingType);
|
||||
|
||||
}
|
||||
|
||||
@@ -322,6 +323,7 @@ public final class LoanApplicationTerms {
|
||||
isScheduleExtensionForDownPaymentDisabled = loanProductRelatedDetail.isDisableScheduleExtensionForDownPayment();
|
||||
}
|
||||
LoanScheduleType loanScheduleType = loanProductRelatedDetail.getLoanScheduleType();
|
||||
LoanScheduleProcessingType loanScheduleProcessingType = loanProductRelatedDetail.getLoanScheduleProcessingType();
|
||||
return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
|
||||
repaymentEvery, repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod,
|
||||
interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate,
|
||||
@@ -338,7 +340,7 @@ public final class LoanApplicationTerms {
|
||||
isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
|
||||
isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment,
|
||||
isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled,
|
||||
loanScheduleType);
|
||||
loanScheduleType, loanScheduleProcessingType);
|
||||
}
|
||||
|
||||
private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
|
||||
@@ -367,7 +369,8 @@ public final class LoanApplicationTerms {
|
||||
final boolean isPrincipalCompoundingDisabledForOverdueLoans, final boolean isDownPaymentEnabled,
|
||||
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean isAutoRepaymentForDownPaymentEnabled,
|
||||
final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate,
|
||||
final boolean isScheduleExtensionForDownPaymentDisabled, final LoanScheduleType loanScheduleType) {
|
||||
final boolean isScheduleExtensionForDownPaymentDisabled, final LoanScheduleType loanScheduleType,
|
||||
final LoanScheduleProcessingType loanScheduleProcessingType) {
|
||||
|
||||
this.currency = currency;
|
||||
this.loanTermFrequency = loanTermFrequency;
|
||||
@@ -452,6 +455,7 @@ public final class LoanApplicationTerms {
|
||||
this.submittedOnDate = submittedOnDate;
|
||||
this.isScheduleExtensionForDownPaymentDisabled = isScheduleExtensionForDownPaymentDisabled;
|
||||
this.loanScheduleType = loanScheduleType;
|
||||
this.loanScheduleProcessingType = loanScheduleProcessingType;
|
||||
}
|
||||
|
||||
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
|
||||
@@ -1295,7 +1299,7 @@ public final class LoanApplicationTerms {
|
||||
this.graceOnArrearsAgeing, this.daysInMonthType.getValue(), this.daysInYearType.getValue(),
|
||||
this.interestRecalculationEnabled, this.isEqualAmortization, this.isDownPaymentEnabled,
|
||||
this.disbursedAmountPercentageForDownPayment, this.isAutoRepaymentForDownPaymentEnabled,
|
||||
this.isScheduleExtensionForDownPaymentDisabled, this.loanScheduleType);
|
||||
this.isScheduleExtensionForDownPaymentDisabled, this.loanScheduleType, this.loanScheduleProcessingType);
|
||||
}
|
||||
|
||||
public Integer getLoanTermFrequency() {
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum LoanScheduleProcessingType {
|
||||
|
||||
HORIZONTAL("Horizontal"), VERTICAL("Vertical");
|
||||
|
||||
private final String humanReadableName;
|
||||
|
||||
public static List<EnumOptionData> getValuesAsEnumOptionDataList() {
|
||||
return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList();
|
||||
}
|
||||
|
||||
public EnumOptionData asEnumOptionData() {
|
||||
return new EnumOptionData((long) this.ordinal(), this.name(), this.humanReadableName);
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -18,6 +18,25 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum LoanScheduleType {
|
||||
CUMULATIVE, PROGRESSIVE
|
||||
|
||||
CUMULATIVE("Cumulative"), PROGRESSIVE("Progressive");
|
||||
|
||||
private final String humanReadableName;
|
||||
|
||||
public static List<EnumOptionData> getValuesAsEnumOptionDataList() {
|
||||
return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList();
|
||||
}
|
||||
|
||||
public EnumOptionData asEnumOptionData() {
|
||||
return new EnumOptionData((long) this.ordinal(), this.name(), this.humanReadableName);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -158,5 +158,6 @@ public interface LoanProductConstants {
|
||||
|
||||
// loan schedule type
|
||||
String LOAN_SCHEDULE_TYPE = "loanScheduleType";
|
||||
String LOAN_SCHEDULE_PROCESSING_TYPE = "loanScheduleProcessingType";
|
||||
|
||||
}
|
||||
|
||||
+29
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanproduct.domain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.IntStream;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
@@ -72,6 +73,34 @@ public class AdvancedPaymentAllocationsValidator {
|
||||
}
|
||||
}
|
||||
|
||||
public void checkGroupingOfAllocationRules(List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules) {
|
||||
loanProductPaymentAllocationRules.forEach(paymentAllocationRule -> {
|
||||
AtomicInteger pastDueRuleInteger = new AtomicInteger();
|
||||
AtomicInteger dueRuleInteger = new AtomicInteger();
|
||||
AtomicInteger inAdvanceRuleInteger = new AtomicInteger();
|
||||
paymentAllocationRule.getAllocationTypes().forEach(paymentAllocationType -> {
|
||||
validateAllocationType(paymentAllocationType, pastDueRuleInteger, dueRuleInteger, inAdvanceRuleInteger);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void validateAllocationType(PaymentAllocationType paymentAllocationType, AtomicInteger pastDueRuleInteger,
|
||||
AtomicInteger dueRuleInteger, AtomicInteger inAdvanceRuleInteger) {
|
||||
switch (paymentAllocationType.getDueType()) {
|
||||
case PAST_DUE -> validateDueType(pastDueRuleInteger, dueRuleInteger, inAdvanceRuleInteger);
|
||||
case DUE -> validateDueType(dueRuleInteger, pastDueRuleInteger, inAdvanceRuleInteger);
|
||||
case IN_ADVANCE -> validateDueType(inAdvanceRuleInteger, pastDueRuleInteger, dueRuleInteger);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDueType(AtomicInteger currentRuleInteger, AtomicInteger otherRule1Integer, AtomicInteger otherRule2Integer) {
|
||||
currentRuleInteger.incrementAndGet();
|
||||
if ((otherRule1Integer.get() > 0 && otherRule1Integer.get() < 4) || (otherRule2Integer.get() > 0 && otherRule2Integer.get() < 4)) {
|
||||
raiseValidationError("mixed.due.type.allocation.rules.are.not.supported.with.horizontal.installment.processing",
|
||||
"Horizontal repayment schedule processing is not supporting mixed due type allocation rules!");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasDuplicateTransactionTypes(List<LoanProductPaymentAllocationRule> rules) {
|
||||
return rules != null
|
||||
&& rules.stream().map(LoanProductPaymentAllocationRule::getTransactionType).distinct().toList().size() != rules.size();
|
||||
|
||||
+13
-3
@@ -62,6 +62,7 @@ import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData;
|
||||
import org.apache.fineract.portfolio.floatingrates.domain.FloatingRate;
|
||||
import org.apache.fineract.portfolio.fund.domain.Fund;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
|
||||
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException;
|
||||
@@ -312,6 +313,15 @@ public class LoanProduct extends AbstractPersistableCustom {
|
||||
loanScheduleType = LoanScheduleType.CUMULATIVE;
|
||||
}
|
||||
|
||||
final LoanScheduleProcessingType loanScheduleProcessingType;
|
||||
if (LoanScheduleType.PROGRESSIVE.equals(loanScheduleType) && command.hasParameter("loanScheduleProcessingType")) {
|
||||
loanScheduleProcessingType = LoanScheduleProcessingType
|
||||
.valueOf(command.stringValueOfParameterNamed("loanScheduleProcessingType"));
|
||||
} else {
|
||||
// For backward compatibility
|
||||
loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL;
|
||||
}
|
||||
|
||||
final boolean useBorrowerCycle = command
|
||||
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME);
|
||||
final Set<LoanProductBorrowerCycleVariations> loanProductBorrowerCycleVariations = new HashSet<>();
|
||||
@@ -436,7 +446,7 @@ public class LoanProduct extends AbstractPersistableCustom {
|
||||
disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber,
|
||||
dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment,
|
||||
enableAutoRepaymentForDownPayment, repaymentStartDateType, disableScheduleExtensionForDownPayment,
|
||||
enableInstallmentLevelDelinquency, loanScheduleType);
|
||||
enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType);
|
||||
|
||||
}
|
||||
|
||||
@@ -652,7 +662,7 @@ public class LoanProduct extends AbstractPersistableCustom {
|
||||
final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
|
||||
final boolean enableAutoRepaymentForDownPayment, final RepaymentStartDateType repaymentStartDateType,
|
||||
final boolean disableScheduleExtensionForDownPayment, final boolean enableInstallmentLevelDelinquency,
|
||||
final LoanScheduleType loanScheduleType) {
|
||||
final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
|
||||
this.fund = fund;
|
||||
this.transactionProcessingStrategyCode = transactionProcessingStrategyCode;
|
||||
|
||||
@@ -694,7 +704,7 @@ public class LoanProduct extends AbstractPersistableCustom {
|
||||
recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod,
|
||||
inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType.getValue(), daysInYearType.getValue(),
|
||||
isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment,
|
||||
enableAutoRepaymentForDownPayment, disableScheduleExtensionForDownPayment, loanScheduleType);
|
||||
enableAutoRepaymentForDownPayment, disableScheduleExtensionForDownPayment, loanScheduleType, loanScheduleProcessingType);
|
||||
|
||||
this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings();
|
||||
|
||||
|
||||
+13
-3
@@ -39,6 +39,7 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType;
|
||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
|
||||
|
||||
@@ -146,6 +147,10 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
@Enumerated(EnumType.STRING)
|
||||
private LoanScheduleType loanScheduleType;
|
||||
|
||||
@Column(name = "loan_schedule_processing_type", nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private LoanScheduleProcessingType loanScheduleProcessingType;
|
||||
|
||||
public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currency, final BigDecimal principal,
|
||||
final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType,
|
||||
final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod,
|
||||
@@ -157,7 +162,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization,
|
||||
final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
|
||||
final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment,
|
||||
final LoanScheduleType loanScheduleType) {
|
||||
final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
|
||||
|
||||
return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType,
|
||||
nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion,
|
||||
@@ -165,7 +170,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod,
|
||||
inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
|
||||
isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment,
|
||||
disableScheduleExtensionForDownPayment, loanScheduleType);
|
||||
disableScheduleExtensionForDownPayment, loanScheduleType, loanScheduleProcessingType);
|
||||
}
|
||||
|
||||
protected LoanProductRelatedDetail() {
|
||||
@@ -183,7 +188,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization,
|
||||
final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment,
|
||||
final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment,
|
||||
final LoanScheduleType loanScheduleType) {
|
||||
final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType) {
|
||||
this.currency = currency;
|
||||
this.principal = defaultPrincipal;
|
||||
this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod;
|
||||
@@ -215,6 +220,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment;
|
||||
this.disableScheduleExtensionForDownPayment = disableScheduleExtensionForDownPayment;
|
||||
this.loanScheduleType = loanScheduleType;
|
||||
this.loanScheduleProcessingType = loanScheduleProcessingType;
|
||||
}
|
||||
|
||||
private Integer defaultToNullIfZero(final Integer value) {
|
||||
@@ -740,4 +746,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
|
||||
public LoanScheduleType getLoanScheduleType() {
|
||||
return loanScheduleType;
|
||||
}
|
||||
|
||||
public LoanScheduleProcessingType getLoanScheduleProcessingType() {
|
||||
return loanScheduleProcessingType;
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -34,4 +34,5 @@
|
||||
<include relativeToChangelogFile="true" file="parts/1009_refactor_loan_Installment_delinquency_tag.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/1010_introduce_loan_schedule_type_configuration.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/1011_add_delinquency_actions_table.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/1012_introduce_loan_schedule_processing_type_configuration.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<addColumn tableName="m_product_loan">
|
||||
<column defaultValue="HORIZONTAL" type="varchar(20)" name="loan_schedule_processing_type">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<addColumn tableName="m_loan">
|
||||
<column defaultValue="HORIZONTAL" type="varchar(20)" name="loan_schedule_processing_type">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+51
-9
@@ -143,11 +143,37 @@ class AdvancedPaymentAllocationsValidatorTest {
|
||||
() -> underTest.validate(List.of(lppr1, lppr2), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkGroupingOfAllocationRulesGoodOrder() {
|
||||
List<LoanProductPaymentAllocationRule> allocationRules1 = List.of(createLoanProductAllocationRule1());
|
||||
Assertions.assertDoesNotThrow(() -> underTest.checkGroupingOfAllocationRules(allocationRules1));
|
||||
|
||||
List<LoanProductPaymentAllocationRule> allocationRules2 = List.of(createLoanProductAllocationRule4());
|
||||
Assertions.assertDoesNotThrow(() -> underTest.checkGroupingOfAllocationRules(allocationRules2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkGroupingOfAllocationRulesWrongOrder() {
|
||||
List<LoanProductPaymentAllocationRule> allocationRules = List.of(createLoanProductAllocationRule3());
|
||||
assertPlatformValidationException("Horizontal repayment schedule processing is not supporting mixed due type allocation rules!",
|
||||
"mixed.due.type.allocation.rules.are.not.supported.with.horizontal.installment.processing",
|
||||
() -> underTest.checkGroupingOfAllocationRules(allocationRules));
|
||||
}
|
||||
|
||||
private void assertPlatformValidationException(String message, String code, Executable executable) {
|
||||
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, executable);
|
||||
assertPlatformException(message, code, validationException);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule1() {
|
||||
LoanProductPaymentAllocationRule lppr1 = new LoanProductPaymentAllocationRule();
|
||||
lppr1.setTransactionType(DEFAULT);
|
||||
lppr1.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
|
||||
lppr1.setAllocationTypes(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
|
||||
return lppr1;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule2() {
|
||||
LoanProductPaymentAllocationRule lppr2 = new LoanProductPaymentAllocationRule();
|
||||
@@ -160,20 +186,36 @@ class AdvancedPaymentAllocationsValidatorTest {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule1() {
|
||||
LoanProductPaymentAllocationRule lppr1 = new LoanProductPaymentAllocationRule();
|
||||
lppr1.setTransactionType(DEFAULT);
|
||||
lppr1.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
|
||||
lppr1.setAllocationTypes(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
|
||||
return lppr1;
|
||||
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule3() {
|
||||
LoanProductPaymentAllocationRule lppr = new LoanProductPaymentAllocationRule();
|
||||
lppr.setTransactionType(REPAYMENT);
|
||||
lppr.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
|
||||
ArrayList<PaymentAllocationType> allocationTypes = new ArrayList<>(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
|
||||
Collections.swap(allocationTypes, 0, 4);
|
||||
Collections.swap(allocationTypes, 3, 8);
|
||||
Collections.swap(allocationTypes, 7, 11);
|
||||
lppr.setAllocationTypes(allocationTypes);
|
||||
return lppr;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule4() {
|
||||
LoanProductPaymentAllocationRule lppr = new LoanProductPaymentAllocationRule();
|
||||
lppr.setTransactionType(DEFAULT);
|
||||
lppr.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
|
||||
ArrayList<PaymentAllocationType> allocationTypes = new ArrayList<>(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
|
||||
Collections.swap(allocationTypes, 0, 4);
|
||||
Collections.swap(allocationTypes, 1, 5);
|
||||
Collections.swap(allocationTypes, 2, 6);
|
||||
Collections.swap(allocationTypes, 3, 7);
|
||||
lppr.setAllocationTypes(allocationTypes);
|
||||
return lppr;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<Pair<Integer, PaymentAllocationType>> createPaymentAllocationTypeList() {
|
||||
AtomicInteger i = new AtomicInteger(1);
|
||||
List<Pair<Integer, PaymentAllocationType>> list = EnumSet.allOf(PaymentAllocationType.class).stream()
|
||||
.map(p -> Pair.of(i.getAndIncrement(), p)).toList();
|
||||
return list;
|
||||
return EnumSet.allOf(PaymentAllocationType.class).stream().map(p -> Pair.of(i.getAndIncrement(), p)).toList();
|
||||
}
|
||||
|
||||
private void assertPlatformException(String expectedMessage, String expectedCode,
|
||||
|
||||
+4
-2
@@ -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.loanschedule.data.LoanScheduleData;
|
||||
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.LoanScheduleHistoryReadPlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService;
|
||||
@@ -1060,8 +1062,8 @@ public class LoansApiResource {
|
||||
repaymentStrategyOptions, interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions,
|
||||
interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, allowedLoanOfficers, loanPurposeOptions,
|
||||
loanCollateralOptions, calendarOptions, notes, accountLinkingOptions, linkedAccount, disbursementData, emiAmountVariations,
|
||||
overdueCharges, paidInAdvanceTemplate, interestRatesPeriods, clientActiveLoanOptions, rates, isRatesEnabled,
|
||||
collectionData);
|
||||
overdueCharges, paidInAdvanceTemplate, interestRatesPeriods, clientActiveLoanOptions, rates, isRatesEnabled, collectionData,
|
||||
LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList());
|
||||
|
||||
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(),
|
||||
mandatoryResponseParameters);
|
||||
|
||||
+13
@@ -23,6 +23,7 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiResourceSwagger.GetDelinquencyRangesResponse;
|
||||
|
||||
/**
|
||||
@@ -1086,6 +1087,10 @@ final class LoansApiResourceSwagger {
|
||||
public Boolean enableAutoRepaymentForDownPayment;
|
||||
@Schema(example = "false")
|
||||
public Boolean disableScheduleExtensionForDownPayment;
|
||||
@Schema(example = "CUMULATIVE")
|
||||
public EnumOptionData loanScheduleType;
|
||||
@Schema(example = "HORIZONTAL")
|
||||
public EnumOptionData loanScheduleProcessingType;
|
||||
}
|
||||
|
||||
@Schema(description = "GetLoansResponse")
|
||||
@@ -1169,6 +1174,10 @@ final class LoansApiResourceSwagger {
|
||||
public Integer graceOnInterestPayment;
|
||||
@Schema(example = "1")
|
||||
public Integer graceOnArrearsAgeing;
|
||||
@Schema(example = "CUMULATIVE")
|
||||
public String loanScheduleType;
|
||||
@Schema(example = "HORIZONTAL")
|
||||
public String loanScheduleProcessingType;
|
||||
}
|
||||
|
||||
@Schema(description = "PostLoansResponse")
|
||||
@@ -1312,6 +1321,10 @@ final class LoansApiResourceSwagger {
|
||||
public List<PutLoansLoanIdChanges> charges;
|
||||
public List<PutLoansLoanIdCollateral> collateral;
|
||||
public List<PutLoansLoanIdDisbursementData> disbursementData;
|
||||
@Schema(example = "CUMULATIVE")
|
||||
public String loanScheduleType;
|
||||
@Schema(example = "HORIZONTAL")
|
||||
public String loanScheduleProcessingType;
|
||||
|
||||
static final class PutLoansLoanIdChanges {
|
||||
|
||||
|
||||
+47
-15
@@ -166,6 +166,8 @@ public class LoanAccountData {
|
||||
private Collection<ChargeData> chargeOptions;
|
||||
private Collection<CodeValueData> loanCollateralOptions;
|
||||
private Collection<CalendarData> calendarOptions;
|
||||
private List<EnumOptionData> loanScheduleTypeOptions;
|
||||
private List<EnumOptionData> loanScheduleProcessingTypeOptions;
|
||||
|
||||
@Transient
|
||||
private BigDecimal feeChargesAtDisbursementCharged;
|
||||
@@ -262,6 +264,9 @@ public class LoanAccountData {
|
||||
private Boolean enableAutoRepaymentForDownPayment;
|
||||
private Boolean disableScheduleExtensionForDownPayment;
|
||||
|
||||
private EnumOptionData loanScheduleType;
|
||||
private EnumOptionData loanScheduleProcessingType;
|
||||
|
||||
public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEnumOption, Long clientId, Long productId,
|
||||
Long loanOfficerId, LocalDate submittedOnDate, Long fundId, BigDecimal principal, Integer numberOfRepayments,
|
||||
Integer repaymentEvery, EnumOptionData repaidEveryFrequencyEnums, Integer loanTermFrequency,
|
||||
@@ -389,7 +394,10 @@ public class LoanAccountData {
|
||||
.setIsEqualAmortization(acc.isEqualAmortization).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
|
||||
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
|
||||
.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)
|
||||
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
|
||||
.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,
|
||||
@@ -472,7 +483,8 @@ public class LoanAccountData {
|
||||
final Collection<EnumOptionData> interestTypeOptions, final Collection<EnumOptionData> interestCalculationPeriodTypeOptions,
|
||||
final Collection<FundData> fundOptions, final Collection<ChargeData> chargeOptions,
|
||||
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();
|
||||
|
||||
@@ -562,7 +574,10 @@ public class LoanAccountData {
|
||||
.setMaximumGap(product.getMaximumGap()).setTopup(product.isCanUseForTopup())
|
||||
.setClientActiveLoanOptions(clientActiveLoanOptions).setIsEqualAmortization(product.isEqualAmortization())
|
||||
.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) {
|
||||
@@ -628,7 +643,8 @@ public class LoanAccountData {
|
||||
.setIsEqualAmortization(product.isEqualAmortization()).setRates(acc.rates).setIsRatesEnabled(acc.isRatesEnabled)
|
||||
.setFixedPrincipalPercentagePerInstallment(product.getFixedPrincipalPercentagePerInstallment()).setDelinquent(delinquent)
|
||||
.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,
|
||||
LocalDate lastClosedBusinessDate, LocalDate overpaidOnDate, final boolean chargedOff, final boolean enableDownPayment,
|
||||
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();
|
||||
|
||||
@@ -708,7 +725,8 @@ public class LoanAccountData {
|
||||
.setEnableDownPayment(enableDownPayment).setDisbursedAmountPercentageForDownPayment(disbursedAmountPercentageForDownPayment)
|
||||
.setEnableAutoRepaymentForDownPayment(enableAutoRepaymentForDownPayment)
|
||||
.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 PaidInAdvanceData paidInAdvance, Collection<InterestRatePeriodData> interestRatesPeriods,
|
||||
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)
|
||||
|
||||
@@ -797,7 +816,9 @@ public class LoanAccountData {
|
||||
.setDisbursedAmountPercentageForDownPayment(acc.disbursedAmountPercentageForDownPayment)
|
||||
.setEnableAutoRepaymentForDownPayment(acc.enableAutoRepaymentForDownPayment)
|
||||
.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,
|
||||
@@ -811,7 +832,8 @@ public class LoanAccountData {
|
||||
acc.interestTypeOptions, acc.interestCalculationPeriodTypeOptions, acc.fundOptions, acc.chargeOptions, null,
|
||||
allowedLoanOfficers, acc.loanPurposeOptions, acc.loanCollateralOptions, calendarOptions, acc.notes, accountLinkingOptions,
|
||||
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) {
|
||||
@@ -873,7 +895,9 @@ public class LoanAccountData {
|
||||
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
|
||||
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
|
||||
.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) {
|
||||
@@ -971,7 +995,9 @@ public class LoanAccountData {
|
||||
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
|
||||
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
|
||||
.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,
|
||||
@@ -1037,7 +1063,9 @@ public class LoanAccountData {
|
||||
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
|
||||
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
|
||||
.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) {
|
||||
@@ -1096,7 +1124,9 @@ public class LoanAccountData {
|
||||
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
|
||||
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
|
||||
.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) {
|
||||
@@ -1158,7 +1188,9 @@ public class LoanAccountData {
|
||||
.setFixedPrincipalPercentagePerInstallment(acc.fixedPrincipalPercentagePerInstallment).setDelinquent(acc.delinquent)
|
||||
.setDelinquencyRange(acc.delinquencyRange).setDisallowExpectedDisbursements(acc.disallowExpectedDisbursements)
|
||||
.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) -> {
|
||||
|
||||
+12
-1
@@ -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.LoanScheduleGeneratorFactory;
|
||||
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.serialization.VariableLoanScheduleFromApiJsonValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler;
|
||||
@@ -472,7 +473,17 @@ public class LoanScheduleAssembler {
|
||||
.isDisableScheduleExtensionForDownPayment();
|
||||
|
||||
}
|
||||
|
||||
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,
|
||||
repaymentEvery, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod,
|
||||
interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
|
||||
@@ -487,7 +498,7 @@ public class LoanScheduleAssembler {
|
||||
allowCompoundingOnEod, isEqualAmortization, isInterestToBeRecoveredFirstWhenGreaterThanEMI,
|
||||
fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled,
|
||||
disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate,
|
||||
isScheduleExtensionForDownPaymentDisabled, loanScheduleType);
|
||||
isScheduleExtensionForDownPaymentDisabled, loanScheduleType, loanScheduleProcessingType);
|
||||
}
|
||||
|
||||
private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery,
|
||||
|
||||
+65
-2
@@ -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.domain.Loan;
|
||||
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.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.domain.AdvancedPaymentAllocationsValidator;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
|
||||
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.savings.domain.SavingsAccount;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -99,7 +104,8 @@ public final class LoanApplicationCommandFromApiJsonHelper {
|
||||
LoanApiConstants.applicationId, // glim specific
|
||||
LoanApiConstants.lastApplication, // glim specific
|
||||
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";
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
@@ -107,6 +113,7 @@ public final class LoanApplicationCommandFromApiJsonHelper {
|
||||
private final ClientCollateralManagementRepositoryWrapper clientCollateralManagementRepositoryWrapper;
|
||||
private final LoanChargeApiJsonValidator loanChargeApiJsonValidator;
|
||||
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
|
||||
private final AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator;
|
||||
|
||||
public void validateForCreate(final String json, final boolean isMeetingMandatoryForJLGLoans, final LoanProduct loanProduct) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
@@ -543,6 +550,33 @@ public final class LoanApplicationCommandFromApiJsonHelper {
|
||||
}
|
||||
|
||||
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);
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException(dataValidationErrors);
|
||||
@@ -625,9 +659,10 @@ public final class LoanApplicationCommandFromApiJsonHelper {
|
||||
.integerGreaterThanZero();
|
||||
}
|
||||
|
||||
String transactionProcessingStrategy = existingLoanApplication.getTransactionProcessingStrategyCode();
|
||||
if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element)) {
|
||||
atLeastOneParameterPassedForUpdate = true;
|
||||
final String transactionProcessingStrategy = this.fromApiJsonHelper
|
||||
transactionProcessingStrategy = this.fromApiJsonHelper
|
||||
.extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element);
|
||||
baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName)
|
||||
.value(transactionProcessingStrategy).notNull();
|
||||
@@ -1049,6 +1084,34 @@ public final class LoanApplicationCommandFromApiJsonHelper {
|
||||
validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal);
|
||||
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()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
|
||||
+11
-3
@@ -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.LoanSchedulePeriodData;
|
||||
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.loanproduct.data.LoanProductData;
|
||||
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, "
|
||||
+ " 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,"
|
||||
+ " 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" //
|
||||
+ " 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."
|
||||
@@ -1044,6 +1046,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
final boolean enableAutoRepaymentForDownPayment = rs.getBoolean("enableAutoRepaymentForDownPayment");
|
||||
final boolean disableScheduleExtensionForDownPayment = rs.getBoolean("disableScheduleExtensionForDownPayment");
|
||||
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,
|
||||
clientOfficeId, clientExternalId, groupData, loanType, loanProductId, loanProductName, loanProductDescription,
|
||||
@@ -1061,7 +1067,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
canUseForTopup, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization,
|
||||
fixedPrincipalPercentagePerInstallment, delinquencyRange, disallowExpectedDisbursements, isFraud,
|
||||
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,
|
||||
repaymentFrequencyNthDayTypeOptions, repaymentFrequencyDaysOfWeekTypeOptions, repaymentStrategyOptions,
|
||||
interestRateFrequencyTypeOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions,
|
||||
fundOptions, chargeOptions, loanPurposeOptions, loanCollateralOptions, loanCycleCounter, activeLoanOptions);
|
||||
fundOptions, chargeOptions, loanPurposeOptions, loanCollateralOptions, loanCycleCounter, activeLoanOptions,
|
||||
LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+4
-1
@@ -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.service.FundReadPlatformService;
|
||||
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.data.LoanProductData;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData;
|
||||
@@ -424,7 +426,8 @@ public class LoanProductsApiResource {
|
||||
preCloseInterestCalculationStrategyOptions, floatingRateOptions, interestRecalculationNthDayTypeOptions,
|
||||
interestRecalculationDayOfWeekTypeOptions, isRatesEnabled, delinquencyBucketOptions, repaymentStartDateTypeOptions,
|
||||
advancedPaymentAllocationTransactionTypes, advancedPaymentAllocationFutureInstallmentAllocationRules,
|
||||
advancedPaymentAllocationTypes);
|
||||
advancedPaymentAllocationTypes, LoanScheduleType.getValuesAsEnumOptionDataList(),
|
||||
LoanScheduleProcessingType.getValuesAsEnumOptionDataList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-3
@@ -258,9 +258,10 @@ final class LoanProductsApiResourceSwagger {
|
||||
|
||||
public AllowAttributeOverrides allowAttributeOverrides;
|
||||
public List<RateData> rates;
|
||||
|
||||
@Schema(example = "CUMULATIVE")
|
||||
public String loanScheduleType;
|
||||
@Schema(example = "HORIZONTAL")
|
||||
public String loanScheduleProcessingType;
|
||||
|
||||
static final class AllowAttributeOverrides {
|
||||
|
||||
@@ -282,8 +283,6 @@ final class LoanProductsApiResourceSwagger {
|
||||
public boolean graceOnPrincipalAndInterestPayment;
|
||||
@Schema(example = "true")
|
||||
public boolean graceOnArrearsAgeing;
|
||||
@Schema(example = "CUMULATIVE")
|
||||
public String loanScheduleType;
|
||||
}
|
||||
|
||||
static final class ChargeData {
|
||||
@@ -1043,6 +1042,8 @@ final class LoanProductsApiResourceSwagger {
|
||||
public List<EnumOptionData> advancedPaymentAllocationTransactionTypes;
|
||||
public List<EnumOptionData> advancedPaymentAllocationFutureInstallmentAllocationRules;
|
||||
public List<EnumOptionData> advancedPaymentAllocationTypes;
|
||||
public List<EnumOptionData> loanScheduleTypeOptions;
|
||||
public List<EnumOptionData> loanScheduleProcessingTypeOptions;
|
||||
}
|
||||
|
||||
@Schema(description = "GetLoanProductsProductIdResponse")
|
||||
@@ -1263,6 +1264,10 @@ final class LoanProductsApiResourceSwagger {
|
||||
public GetLoanProductsRepaymentStartDateType repaymentStartDateType;
|
||||
@Schema(example = "false")
|
||||
public Boolean disableScheduleExtensionForDownPayment;
|
||||
@Schema(example = "CUMULATIVE")
|
||||
public EnumOptionData loanScheduleType;
|
||||
@Schema(example = "HORIZONTAL")
|
||||
public EnumOptionData loanScheduleProcessingType;
|
||||
}
|
||||
|
||||
@Schema(description = "PutLoanProductsProductIdRequest")
|
||||
|
||||
+37
-6
@@ -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.fund.data.FundData;
|
||||
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.FutureInstallmentAllocationRule;
|
||||
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> advancedPaymentAllocationFutureInstallmentAllocationRules;
|
||||
private final List<EnumOptionData> advancedPaymentAllocationTypes;
|
||||
private final List<EnumOptionData> loanScheduleTypeOptions;
|
||||
private final List<EnumOptionData> loanScheduleProcessingTypeOptions;
|
||||
|
||||
private final List<EnumOptionData> interestRecalculationFrequencyTypeOptions;
|
||||
private final List<FloatingRateData> floatingRateOptions;
|
||||
@@ -209,6 +213,9 @@ public class LoanProductData implements Serializable {
|
||||
private final boolean disableScheduleExtensionForDownPayment;
|
||||
private final boolean enableInstallmentLevelDelinquency;
|
||||
|
||||
private final EnumOptionData loanScheduleType;
|
||||
private final EnumOptionData loanScheduleProcessingType;
|
||||
|
||||
/**
|
||||
* Used when returning lookup information about loan product for dropdowns.
|
||||
*/
|
||||
@@ -304,6 +311,10 @@ public class LoanProductData implements Serializable {
|
||||
final EnumOptionData repaymentStartDateType = null;
|
||||
final boolean disableScheduleExtensionForDownPayment = 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,
|
||||
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
|
||||
@@ -323,7 +334,8 @@ public class LoanProductData implements Serializable {
|
||||
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
|
||||
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
|
||||
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 boolean disableScheduleExtensionForDownPayment = 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,
|
||||
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
|
||||
@@ -439,7 +453,8 @@ public class LoanProductData implements Serializable {
|
||||
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
|
||||
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
|
||||
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 boolean disableScheduleExtensionForDownPayment = 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,
|
||||
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
|
||||
@@ -562,7 +579,8 @@ public class LoanProductData implements Serializable {
|
||||
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
|
||||
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
|
||||
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 boolean disableScheduleExtensionForDownPayment = 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,
|
||||
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
|
||||
@@ -679,7 +699,8 @@ public class LoanProductData implements Serializable {
|
||||
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
|
||||
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
|
||||
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,
|
||||
@@ -728,7 +749,8 @@ public class LoanProductData implements Serializable {
|
||||
final Integer overDueDaysForRepaymentEvent, final boolean enableDownPayment,
|
||||
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment,
|
||||
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.name = name;
|
||||
this.shortName = shortName;
|
||||
@@ -857,6 +879,10 @@ public class LoanProductData implements Serializable {
|
||||
this.advancedPaymentAllocationTypes = PaymentAllocationType.getValuesAsEnumOptionDataList();
|
||||
this.disableScheduleExtensionForDownPayment = disableScheduleExtensionForDownPayment;
|
||||
this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency;
|
||||
this.loanScheduleType = loanScheduleType;
|
||||
this.loanScheduleProcessingType = loanScheduleProcessingType;
|
||||
this.loanScheduleTypeOptions = null;
|
||||
this.loanScheduleProcessingTypeOptions = null;
|
||||
}
|
||||
|
||||
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 List<EnumOptionData> advancedPaymentAllocationTransactionTypes,
|
||||
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.name = productData.name;
|
||||
this.shortName = productData.shortName;
|
||||
@@ -1022,6 +1049,10 @@ public class LoanProductData implements Serializable {
|
||||
this.advancedPaymentAllocationTypes = advancedPaymentAllocationTypes;
|
||||
this.disableScheduleExtensionForDownPayment = productData.disableScheduleExtensionForDownPayment;
|
||||
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) {
|
||||
|
||||
+70
-6
@@ -37,6 +37,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
|
||||
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.DataValidatorBuilder;
|
||||
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.loanaccount.api.LoanApiConstants;
|
||||
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.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.InterestCalculationPeriodMethod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy;
|
||||
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.RecalculationFrequencyType;
|
||||
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_AUTO_REPAYMENT_DOWN_PAYMENT, LoanProductConstants.REPAYMENT_START_DATE_TYPE,
|
||||
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,
|
||||
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName,
|
||||
@@ -179,10 +185,12 @@ public final class LoanProductDataValidator {
|
||||
public static final String DOT = ".";
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
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)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
@@ -777,6 +785,28 @@ public final class LoanProductDataValidator {
|
||||
baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_TYPE).value(loanScheduleType).ignoreIfNull()
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -1137,7 +1167,8 @@ public final class LoanProductDataValidator {
|
||||
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)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
@@ -1247,9 +1278,9 @@ public final class LoanProductDataValidator {
|
||||
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)) {
|
||||
final String transactionProcessingStrategyCode = this.fromApiJsonHelper.extractStringNamed(TRANSACTION_PROCESSING_STRATEGY_CODE,
|
||||
element);
|
||||
transactionProcessingStrategyCode = this.fromApiJsonHelper.extractStringNamed(TRANSACTION_PROCESSING_STRATEGY_CODE, element);
|
||||
baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).value(transactionProcessingStrategyCode).notBlank();
|
||||
// Validating whether the processor is existing
|
||||
loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode);
|
||||
@@ -1753,6 +1784,39 @@ public final class LoanProductDataValidator {
|
||||
.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);
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -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.delinquency.data.DelinquencyBucketData;
|
||||
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.PaymentAllocationOrder;
|
||||
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, "
|
||||
+ "lp.allow_variabe_installments as isVariableIntallmentsAllowed, " + "lvi.minimum_gap as minimumGap, "
|
||||
+ "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 "
|
||||
+ " 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 "
|
||||
@@ -505,6 +507,11 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
|
||||
final DelinquencyBucketData delinquencyBucket = new DelinquencyBucketData(delinquencyBucketId, delinquencyBucketName,
|
||||
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,
|
||||
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
|
||||
minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType,
|
||||
@@ -525,7 +532,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
|
||||
isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket,
|
||||
dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment,
|
||||
enableAutoRepaymentForDownPayment, advancedPaymentData, repaymentStartDateType, disableScheduleExtensionForDownPayment,
|
||||
enableInstallmentLevelDelinquency);
|
||||
enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -97,7 +97,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
|
||||
|
||||
this.context.authenticatedUser();
|
||||
|
||||
this.fromApiJsonDeserializer.validateForCreate(command.json());
|
||||
this.fromApiJsonDeserializer.validateForCreate(command);
|
||||
validateInputDates(command);
|
||||
|
||||
final Fund fund = findFundByIdIfProvided(command.longValueOfParameterNamed("fundId"));
|
||||
@@ -172,7 +172,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
|
||||
final LoanProduct product = this.loanProductRepository.findById(loanProductId)
|
||||
.orElseThrow(() -> new LoanProductNotFoundException(loanProductId));
|
||||
|
||||
this.fromApiJsonDeserializer.validateForUpdate(command.json(), product);
|
||||
this.fromApiJsonDeserializer.validateForUpdate(command, product);
|
||||
validateInputDates(command);
|
||||
|
||||
if (anyChangeInCriticalFloatingRateLinkedParams(command, product)
|
||||
|
||||
+5
@@ -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.LoanTransaction;
|
||||
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.PaymentAllocationType;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
@@ -182,6 +184,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
Money overpaidAmount = Money.zero(currency);
|
||||
Money zero = Money.zero(currency);
|
||||
Loan loan = Mockito.mock(Loan.class);
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
|
||||
Money chargeAmountMoney = Money.of(currency, chargeAmount);
|
||||
BigDecimal transactionAmount = BigDecimal.valueOf(120.00);
|
||||
Money transactionAmountMoney = Money.of(currency, transactionAmount);
|
||||
@@ -198,6 +201,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
Mockito.when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
|
||||
Mockito.when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
|
||||
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(installment.isFirstPeriod()).thenReturn(true);
|
||||
Mockito.when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate()))
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ public class DefaultScheduledDateGeneratorTest {
|
||||
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,
|
||||
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() {
|
||||
|
||||
+401
-16
@@ -20,6 +20,7 @@ package org.apache.fineract.integrationtests;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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 io.restassured.builder.RequestSpecBuilder;
|
||||
@@ -29,10 +30,13 @@ import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.fineract.client.models.AdvancedPaymentData;
|
||||
import org.apache.fineract.client.models.BusinessDateRequest;
|
||||
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.PostLoansResponse;
|
||||
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.integrationtests.common.BusinessDateHelper;
|
||||
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.LoanRescheduleRequestHelper;
|
||||
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.LoanTransactionHelper;
|
||||
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.loanproduct.domain.PaymentAllocationType;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
@@ -101,7 +109,8 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
|
||||
final Account expenseAccount = accountHelper.createExpenseAccount();
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -1739,8 +1748,8 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
|
||||
final Account incomeAccount = accountHelper.createIncomeAccount();
|
||||
final Account expenseAccount = accountHelper.createExpenseAccount();
|
||||
final Account overpaymentAccount = accountHelper.createLiabilityAccount();
|
||||
Integer localLoanProductId = createLoanProduct("500", "15", "4", false, assetAccount, incomeAccount, expenseAccount,
|
||||
overpaymentAccount);
|
||||
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");
|
||||
|
||||
@@ -1824,8 +1833,8 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
|
||||
final Account incomeAccount = accountHelper.createIncomeAccount();
|
||||
final Account expenseAccount = accountHelper.createExpenseAccount();
|
||||
final Account overpaymentAccount = accountHelper.createLiabilityAccount();
|
||||
Integer localLoanProductId = createLoanProduct("500", "15", "4", false, assetAccount, incomeAccount, expenseAccount,
|
||||
overpaymentAccount);
|
||||
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");
|
||||
|
||||
@@ -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,
|
||||
Double principalOutstanding, Double principalPaid, Double totalOverpaid) {
|
||||
assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding());
|
||||
@@ -1920,7 +2241,22 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
|
||||
paymentAllocationOrder.setPaymentAllocationRule(pat.name());
|
||||
paymentAllocationOrder.setOrder(integer.getAndIncrement());
|
||||
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() {
|
||||
@@ -1954,7 +2290,44 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
|
||||
}
|
||||
|
||||
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 goodwillCreditAllocation = createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT");
|
||||
AdvancedPaymentData merchantIssuedRefundAllocation = createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION");
|
||||
@@ -1969,8 +2342,9 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
|
||||
.addAdvancedPaymentAllocation(defaultAllocation, goodwillCreditAllocation, merchantIssuedRefundAllocation,
|
||||
payoutRefundAllocation)
|
||||
.withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withInterestTypeAsDecliningBalance().withMultiDisburse()
|
||||
.withDisallowExpectedDisbursements(true).withLoanScheduleType(LoanScheduleType.PROGRESSIVE).withDaysInMonth("30")
|
||||
.withDaysInYear("365").withMoratorium("0", "0").build(null);
|
||||
.withDisallowExpectedDisbursements(true).withLoanScheduleType(loanScheduleType)
|
||||
.withLoanScheduleProcessingType(loanScheduleProcessingType).withDaysInMonth("30").withDaysInYear("365")
|
||||
.withMoratorium("0", "0").build(null);
|
||||
return loanTransactionHelper.getLoanProductId(loanProductJSON);
|
||||
}
|
||||
|
||||
@@ -2022,16 +2396,27 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest {
|
||||
|
||||
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) {
|
||||
final String expectedDisbursementDate, final String submittedOnDate, String transactionProcessorCode, String loanScheduleType,
|
||||
String loanScheduleProcessingType) {
|
||||
LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
|
||||
return loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(clientId).productId(loanProductId.longValue())
|
||||
.expectedDisbursementDate(expectedDisbursementDate).dateFormat(DATETIME_PATTERN)
|
||||
.transactionProcessingStrategyCode(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
|
||||
.locale("en").submittedOnDate(submittedOnDate).amortizationType(1).interestRatePerPeriod(interestRate)
|
||||
.interestCalculationPeriodType(1).interestType(0).repaymentFrequencyType(0).repaymentEvery(repaymentAfterEvery)
|
||||
.repaymentFrequencyType(0).numberOfRepayments(numberOfRepayments).loanTermFrequency(loanTermFrequency)
|
||||
.loanTermFrequencyType(0).principal(BigDecimal.valueOf(principal)).loanType("individual")
|
||||
.maxOutstandingLoanBalance(BigDecimal.valueOf(35000)));
|
||||
.transactionProcessingStrategyCode(transactionProcessorCode).locale("en").submittedOnDate(submittedOnDate)
|
||||
.amortizationType(1).interestRatePerPeriod(interestRate).interestCalculationPeriodType(1).interestType(0)
|
||||
.repaymentFrequencyType(0).repaymentEvery(repaymentAfterEvery).repaymentFrequencyType(0)
|
||||
.numberOfRepayments(numberOfRepayments).loanTermFrequency(loanTermFrequency).loanTermFrequencyType(0)
|
||||
.principal(BigDecimal.valueOf(principal)).loanType("individual").loanScheduleProcessingType(loanScheduleProcessingType)
|
||||
.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,
|
||||
|
||||
+3
@@ -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.LoanTransactionHelper;
|
||||
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.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -294,6 +296,7 @@ public class RefundForActiveLoansWithAdvancedPaymentAllocationTest {
|
||||
.withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments)
|
||||
.withEnableDownPayment(true, "25", true).withinterestRatePerPeriod("0").withInterestRateFrequencyTypeAsMonths()
|
||||
.withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
|
||||
.withLoanScheduleType(LoanScheduleType.PROGRESSIVE).withLoanScheduleProcessingType(LoanScheduleProcessingType.VERTICAL)
|
||||
.withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts)
|
||||
.addAdvancedPaymentAllocation(defaultAllocation).withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0")
|
||||
.build(null);
|
||||
|
||||
+10
-2
@@ -28,6 +28,7 @@ import java.util.Map;
|
||||
import org.apache.fineract.client.models.AdvancedPaymentData;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
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;
|
||||
|
||||
public class LoanProductTestBuilder {
|
||||
@@ -152,7 +153,8 @@ public class LoanProductTestBuilder {
|
||||
private boolean enableAutoRepaymentForDownPayment = false;
|
||||
private Integer repaymentStartDateType = null;
|
||||
private boolean disableScheduleExtensionForDownPayment = false;
|
||||
private LoanScheduleType loanScheduleType = LoanScheduleType.CUMULATIVE;
|
||||
private String loanScheduleType = LoanScheduleType.CUMULATIVE.name();
|
||||
private String loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL.name();
|
||||
|
||||
public String build() {
|
||||
final HashMap<String, Object> map = build(null, null);
|
||||
@@ -310,6 +312,7 @@ public class LoanProductTestBuilder {
|
||||
map.put("disableScheduleExtensionForDownPayment", disableScheduleExtensionForDownPayment);
|
||||
}
|
||||
map.put("loanScheduleType", loanScheduleType);
|
||||
map.put("loanScheduleProcessingType", loanScheduleProcessingType);
|
||||
|
||||
return map;
|
||||
}
|
||||
@@ -751,7 +754,12 @@ public class LoanProductTestBuilder {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user