diff --git a/custom/acme/loan/processor/src/main/java/com/acme/fineract/loan/processor/AcmeLoanRepaymentScheduleTransactionProcessor.java b/custom/acme/loan/processor/src/main/java/com/acme/fineract/loan/processor/AcmeLoanRepaymentScheduleTransactionProcessor.java index f53a94458..dc0953744 100644 --- a/custom/acme/loan/processor/src/main/java/com/acme/fineract/loan/processor/AcmeLoanRepaymentScheduleTransactionProcessor.java +++ b/custom/acme/loan/processor/src/main/java/com/acme/fineract/loan/processor/AcmeLoanRepaymentScheduleTransactionProcessor.java @@ -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() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeEffectiveDueDateComparator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeEffectiveDueDateComparator.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeEffectiveDueDateComparator.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeEffectiveDueDateComparator.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index fd7f81936..0e46345cf 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -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 + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java similarity index 99% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index f93012fda..738ad04a1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -765,7 +765,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); } - private void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set charges, + protected void undoChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set charges, final Integer installmentNumber) { Money amountRemaining = feeCharges; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java similarity index 70% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 709e14f2b..6e6aa29ad 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -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 loanTransactions, + MonetaryCurrency currency, List installments, Set 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 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 installments, Set 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 installments, Set charges) { + Money zero = Money.zero(currency); + List transactionMappings = new ArrayList<>(); + Money transactionAmountUnprocessed = loanTransaction.getAmount(currency); + + List 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 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 loanFees = extractFeeCharges(charges); + final Set 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 installments, Set 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 loanTransactions, - MonetaryCurrency currency, List installments, Set 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 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 createSortedChargesAndTransactionsList(List loanTransactions, Set charges) { @@ -184,26 +268,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep return chargeOrTransactions; } - @Override - public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Set 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 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 installments, Set charges) { - Money zero = Money.zero(currency); - List transactionMappings = new ArrayList<>(); - Money transactionAmountUnprocessed = loanTransaction.getAmount(currency); - - List paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules(); - LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream() - .filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow(); - Optional 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 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 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 installments, Money zero, List 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 installments, Money transactionAmountUnprocessed, + LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, + Balances balances) { + LinkedHashMap> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream() + .collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, + mapping(Function.identity(), toList()))); + + for (Map.Entry> 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 installments, Money transactionAmountUnprocessed, + List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, + List 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 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 installments, Money transactionAmountUnprocessed, + LoanPaymentAllocationRule paymentAllocationRule, List 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 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 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 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 charges) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java similarity index 98% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java index 501f93558..50f167260 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/CreocoreLoanRepaymentScheduleTransactionProcessor.java @@ -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() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java similarity index 98% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java index 2915ef9e4..95ca1bab1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java @@ -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() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java similarity index 98% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java index 09e8d649e..b87ced9bd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java @@ -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() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java similarity index 99% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java index eb3c4273e..6d62063aa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/EarlyPaymentLoanRepaymentScheduleTransactionProcessor.java @@ -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() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/FineractStyleLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/FineractStyleLoanRepaymentScheduleTransactionProcessor.java similarity index 98% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/FineractStyleLoanRepaymentScheduleTransactionProcessor.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/FineractStyleLoanRepaymentScheduleTransactionProcessor.java index fb3ab2f02..8e563299b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/FineractStyleLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/FineractStyleLoanRepaymentScheduleTransactionProcessor.java @@ -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() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java similarity index 99% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java index 3e86d3b1b..984609ec6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java @@ -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() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java similarity index 98% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java index 05af5e8f3..849a8478a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java @@ -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() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java similarity index 98% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java index 898d909be..ff1cd3e9e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor.java @@ -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() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java similarity index 99% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java index 44642f513..fbaa5522c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/RBILoanRepaymentScheduleTransactionProcessor.java @@ -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() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index ce20a975a..e08ca42d6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -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() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleProcessingType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleProcessingType.java new file mode 100644 index 000000000..02bf63045 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleProcessingType.java @@ -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 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); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleType.java index cf6d574b5..f56173377 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleType.java @@ -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 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); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java index c58e7cb50..dc72b3441 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java @@ -158,5 +158,6 @@ public interface LoanProductConstants { // loan schedule type String LOAN_SCHEDULE_TYPE = "loanScheduleType"; + String LOAN_SCHEDULE_PROCESSING_TYPE = "loanScheduleProcessingType"; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsValidator.java index 858eac5fb..127be2034 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsValidator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsValidator.java @@ -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 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 rules) { return rules != null && rules.stream().map(LoanProductPaymentAllocationRule::getTransactionType).distinct().toList().size() != rules.size(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index c18c77409..4ef28165c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -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 = 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(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index 186d163d2..1bbb42fe5 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -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; + } } diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml index fa3e74c5e..25da71bb5 100644 --- a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml @@ -34,4 +34,5 @@ + diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1012_introduce_loan_schedule_processing_type_configuration.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1012_introduce_loan_schedule_processing_type_configuration.xml new file mode 100644 index 000000000..a19e5c679 --- /dev/null +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1012_introduce_loan_schedule_processing_type_configuration.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsValidatorTest.java b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsValidatorTest.java index b3a19d559..ba0d339df 100644 --- a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsValidatorTest.java +++ b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/AdvancedPaymentAllocationsValidatorTest.java @@ -143,11 +143,37 @@ class AdvancedPaymentAllocationsValidatorTest { () -> underTest.validate(List.of(lppr1, lppr2), ADVANCED_PAYMENT_ALLOCATION_STRATEGY)); } + @Test + public void checkGroupingOfAllocationRulesGoodOrder() { + List allocationRules1 = List.of(createLoanProductAllocationRule1()); + Assertions.assertDoesNotThrow(() -> underTest.checkGroupingOfAllocationRules(allocationRules1)); + + List allocationRules2 = List.of(createLoanProductAllocationRule4()); + Assertions.assertDoesNotThrow(() -> underTest.checkGroupingOfAllocationRules(allocationRules2)); + } + + @Test + public void checkGroupingOfAllocationRulesWrongOrder() { + List 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 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 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> createPaymentAllocationTypeList() { AtomicInteger i = new AtomicInteger(1); - List> 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, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index a0c8c10bd..0cfd44249 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index a011f28f6..80a9a8b12 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -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 charges; public List collateral; public List disbursementData; + @Schema(example = "CUMULATIVE") + public String loanScheduleType; + @Schema(example = "HORIZONTAL") + public String loanScheduleProcessingType; static final class PutLoansLoanIdChanges { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java index 105626159..20572c6a8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java @@ -166,6 +166,8 @@ public class LoanAccountData { private Collection chargeOptions; private Collection loanCollateralOptions; private Collection calendarOptions; + private List loanScheduleTypeOptions; + private List 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 interestTypeOptions, final Collection interestCalculationPeriodTypeOptions, final Collection fundOptions, final Collection chargeOptions, final Collection loanPurposeOptions, final Collection loanCollateralOptions, - final Integer loanCycleNumber, final Collection clientActiveLoanOptions) { + final Integer loanCycleNumber, final Collection clientActiveLoanOptions, + final List loanScheduleTypeOptions, final List 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 emiAmountVariations, final Collection overdueCharges, final PaidInAdvanceData paidInAdvance, Collection interestRatesPeriods, final Collection clientActiveLoanOptions, final List rates, final Boolean isRatesEnabled, - final CollectionData delinquent) { + final CollectionData delinquent, final List loanScheduleTypeOptions, + final List 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 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 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 ClientNameComparator = (loan1, loan2) -> { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index ee35a790c..f34269cf7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -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, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java index 82c73c9ae..ca34c369d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java @@ -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 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 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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 349b5b999..f0d024031 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -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 diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index 1f06f9f69..e8fada65a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -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()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java index 15487770a..d652d4659 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java @@ -258,9 +258,10 @@ final class LoanProductsApiResourceSwagger { public AllowAttributeOverrides allowAttributeOverrides; public List 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 advancedPaymentAllocationTransactionTypes; public List advancedPaymentAllocationFutureInstallmentAllocationRules; public List advancedPaymentAllocationTypes; + public List loanScheduleTypeOptions; + public List 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") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java index 49b98021a..4b644380c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java @@ -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 advancedPaymentAllocationTransactionTypes; private final List advancedPaymentAllocationFutureInstallmentAllocationRules; private final List advancedPaymentAllocationTypes; + private final List loanScheduleTypeOptions; + private final List loanScheduleProcessingTypeOptions; private final List interestRecalculationFrequencyTypeOptions; private final List 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 accountingMappings, @@ -728,7 +749,8 @@ public class LoanProductData implements Serializable { final Integer overDueDaysForRepaymentEvent, final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment, final Collection 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 chargeOptions, @@ -876,7 +902,8 @@ public class LoanProductData implements Serializable { final Collection delinquencyBucketOptions, final List repaymentStartDateTypeOptions, final List advancedPaymentAllocationTransactionTypes, final List advancedPaymentAllocationFutureInstallmentAllocationRules, - final List advancedPaymentAllocationTypes) { + final List advancedPaymentAllocationTypes, final List loanScheduleTypeOptions, + final List 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 nullIfEmpty(final Collection charges) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 8b8470cbb..e7d99ccc4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -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 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); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java index f4c20667b..3b2327190 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java @@ -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()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java index 3630cca23..a47fe5e0c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java @@ -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) diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java index 34a5abd91..3856ced6e 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java @@ -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())) diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java index 30adc6181..d2590947b 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java @@ -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() { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 49002bc38..d11694170 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -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> 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 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> 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, diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java index cbe683a6e..e88d237b5 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java @@ -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); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index fc8cba5c7..31ee79fc2 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -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 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; } }