FINERACT-1932: Fineract modularization Fineract progressive loan module

This commit is contained in:
Ruchi Dhamankar
2024-05-24 14:35:34 +02:00
committed by Adam Saghy
parent f59ae64222
commit 53f4864980
53 changed files with 364 additions and 2 deletions
@@ -243,9 +243,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
@Column(name = "loan_transaction_strategy_name")
private String transactionProcessingStrategyName;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after removing association
// from Loan entity
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private List<LoanPaymentAllocationRule> paymentAllocationRules = new ArrayList<>();
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after removing association
// from Loan entity
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private List<LoanCreditAllocationRule> creditAllocationRules = new ArrayList<>();
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanTransaction classes
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanTransaction classes
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
@@ -32,6 +32,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanTransaction classes
@Entity
@Table(name = "m_loan_reage_parameter")
@AllArgsConstructor
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain.reaging;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanTransaction classes
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
@@ -1,98 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.Optional;
import lombok.Getter;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.jetbrains.annotations.NotNull;
@Getter
public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
private final Optional<LoanCharge> loanCharge;
private final Optional<LoanTransaction> loanTransaction;
public ChargeOrTransaction(LoanCharge loanCharge) {
this.loanCharge = Optional.of(loanCharge);
this.loanTransaction = Optional.empty();
}
public ChargeOrTransaction(LoanTransaction loanTransaction) {
this.loanTransaction = Optional.of(loanTransaction);
this.loanCharge = Optional.empty();
}
private LocalDate getEffectiveDate() {
if (loanCharge.isPresent()) {
if (isBackdatedCharge()) {
return loanCharge.get().getDueDate();
} else {
return loanCharge.get().getSubmittedOnDate();
}
} else if (loanTransaction.isPresent()) {
return loanTransaction.get().getTransactionDate();
} else {
throw new RuntimeException("Either charge or transaction should be present");
}
}
private boolean isBackdatedCharge() {
return loanCharge.get().getDueDate().isBefore(loanCharge.get().getSubmittedOnDate());
}
private LocalDate getSubmittedOnDate() {
if (loanCharge.isPresent()) {
return loanCharge.get().getSubmittedOnDate();
} else if (loanTransaction.isPresent()) {
return loanTransaction.get().getSubmittedOnDate();
} else {
throw new RuntimeException("Either charge or transaction should be present");
}
}
private OffsetDateTime getCreatedDateTime() {
if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) {
return loanCharge.get().getCreatedDate().get();
} else if (loanTransaction.isPresent()) {
return loanTransaction.get().getCreatedDateTime();
} else {
throw new RuntimeException("Either charge with createdDate or transaction created datetime should be present");
}
}
@Override
@SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS", justification = "TODO: fix this! See: https://stackoverflow.com/questions/2609037/findbugs-how-to-solve-eq-compareto-use-object-equals")
public int compareTo(@NotNull ChargeOrTransaction o) {
int datePortion = this.getEffectiveDate().compareTo(o.getEffectiveDate());
if (datePortion == 0) {
int submittedDate = this.getSubmittedOnDate().compareTo(o.getSubmittedOnDate());
if (submittedDate == 0) {
return this.getCreatedDateTime().compareTo(o.getCreatedDateTime());
}
return submittedDate;
}
return datePortion;
}
}
@@ -0,0 +1,148 @@
/**
* 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.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Set;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
public final class LoanScheduleModelDownPaymentPeriod implements LoanScheduleModelPeriod {
private final int periodNumber;
private final LocalDate periodDate;
private Money principalDue;
private final Money outstandingLoanBalance;
public static LoanScheduleModelDownPaymentPeriod downPayment(final int periodNumber, final LocalDate periodDate,
final Money principalDue, final Money outstandingLoanBalance) {
return new LoanScheduleModelDownPaymentPeriod(periodNumber, periodDate, principalDue, outstandingLoanBalance);
}
private LoanScheduleModelDownPaymentPeriod(int periodNumber, LocalDate periodDate, Money principalDue, Money outstandingLoanBalance) {
this.periodNumber = periodNumber;
this.periodDate = periodDate;
this.principalDue = principalDue;
this.outstandingLoanBalance = outstandingLoanBalance;
}
@Override
public LoanSchedulePeriodData toData() {
return LoanSchedulePeriodData.downPaymentOnlyPeriod(this.periodNumber, this.periodDate, this.principalDue.getAmount(),
this.outstandingLoanBalance.getAmount());
}
@Override
public boolean isRepaymentPeriod() {
return false;
}
@Override
public boolean isDownPaymentPeriod() {
return true;
}
@Override
public Integer periodNumber() {
return this.periodNumber;
}
@Override
public LocalDate periodFromDate() {
return this.periodDate;
}
@Override
public LocalDate periodDueDate() {
return this.periodDate;
}
@Override
public BigDecimal principalDue() {
BigDecimal value = BigDecimal.ZERO;
if (this.principalDue != null) {
value = this.principalDue.getAmount();
}
return value;
}
@Override
public BigDecimal interestDue() {
return BigDecimal.ZERO;
}
@Override
public BigDecimal feeChargesDue() {
return BigDecimal.ZERO;
}
@Override
public BigDecimal penaltyChargesDue() {
return BigDecimal.ZERO;
}
@Override
public void addLoanCharges(BigDecimal feeCharge, BigDecimal penaltyCharge) {
}
@Override
public boolean isRecalculatedInterestComponent() {
return false;
}
@Override
public void addPrincipalAmount(Money principalDue) {
}
@Override
public void addInterestAmount(Money interestDue) {
}
@Override
public Set<LoanInterestRecalcualtionAdditionalDetails> getLoanCompoundingDetails() {
return null;
}
@Override
public void setEMIFixedSpecificToInstallmentTrue() {
}
@Override
public boolean isEMIFixedSpecificToInstallment() {
return false;
}
@Override
public BigDecimal rescheduleInterestPortion() {
return null;
}
@Override
public void setRescheduleInterestPortion(BigDecimal rescheduleInterestPortion) {
}
}
@@ -0,0 +1,532 @@
/**
* 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.data;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.RecalculationDetail;
public final class LoanScheduleParams {
// Actual period Number as per the schedule
private int periodNumber;
// Actual period Number plus interest only repayments
private int instalmentNumber;
private LocalDate periodStartDate;
private LocalDate actualRepaymentDate;
// variables for cumulative totals
private Money totalCumulativePrincipal;
private Money totalCumulativeInterest;
private Money totalFeeChargesCharged;
private Money totalPenaltyChargesCharged;
private Money totalRepaymentExpected;
private Money totalOutstandingInterestPaymentDueToGrace;
// early payments will be added here and as per the selected strategy
// action will be performed on this value
private Money reducePrincipal;
// principal changes will be added along with date(after applying rest)
// from when these amounts will effect the outstanding balance for
// interest calculation
private final Map<LocalDate, Money> principalPortionMap;
// compounding(principal) amounts will be added along with
// date(after applying compounding frequency)
// from when these amounts will effect the outstanding balance for
// interest calculation
private final Map<LocalDate, Money> latePaymentMap;
// compounding(interest/Fee) amounts will be added along with
// date(after applying compounding frequency)
// from when these amounts will effect the outstanding balance for
// interest calculation
private final Map<LocalDate, Money> compoundingMap;
private final Map<LocalDate, Map<LocalDate, Money>> compoundingDateVariations = new HashMap<>();
private Money unCompoundedAmount;
private Money compoundedInLastInstallment;
public Money getCompoundedInLastInstallment() {
return this.compoundedInLastInstallment;
}
public void setCompoundedInLastInstallment(Money compoundedInLastInstallment) {
this.compoundedInLastInstallment = compoundedInLastInstallment;
}
// disbursement map for tranche details(will added to outstanding
// balance as per the start date)
private final Map<LocalDate, Money> disburseDetailMap;
private Money principalToBeScheduled;
private Money outstandingBalance;
// total outstanding balance as per rest for interest calculation.
private Money outstandingBalanceAsPerRest;
private final List<LoanRepaymentScheduleInstallment> installments;
private final Collection<RecalculationDetail> recalculationDetails;
private final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor;
private final LocalDate scheduleTillDate;
private final boolean partialUpdate;
private int loanTermInDays;
private final MonetaryCurrency currency;
private final boolean applyInterestRecalculation;
private LoanScheduleParams(final int periodNumber, final int instalmentNumber, int loanTermInDays, LocalDate periodStartDate,
final LocalDate actualRepaymentDate, final Money totalCumulativePrincipal, final Money totalCumulativeInterest,
final Money totalFeeChargesCharged, final Money totalPenaltyChargesCharged, final Money totalRepaymentExpected,
Money totalOutstandingInterestPaymentDueToGrace, final Money reducePrincipal, final Map<LocalDate, Money> principalPortionMap,
final Map<LocalDate, Money> latePaymentMap, final Map<LocalDate, Money> compoundingMap, final Money unCompoundedAmount,
final Map<LocalDate, Money> disburseDetailMap, Money principalToBeScheduled, final Money outstandingBalance,
final Money outstandingBalanceAsPerRest, final List<LoanRepaymentScheduleInstallment> installments,
final Collection<RecalculationDetail> recalculationDetails,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate,
final boolean partialUpdate, final MonetaryCurrency currency, final boolean applyInterestRecalculation) {
this.periodNumber = periodNumber;
this.instalmentNumber = instalmentNumber;
this.loanTermInDays = loanTermInDays;
this.periodStartDate = periodStartDate;
this.actualRepaymentDate = actualRepaymentDate;
this.totalCumulativePrincipal = totalCumulativePrincipal;
this.totalCumulativeInterest = totalCumulativeInterest;
this.totalFeeChargesCharged = totalFeeChargesCharged;
this.totalPenaltyChargesCharged = totalPenaltyChargesCharged;
this.totalRepaymentExpected = totalRepaymentExpected;
this.totalOutstandingInterestPaymentDueToGrace = totalOutstandingInterestPaymentDueToGrace;
this.reducePrincipal = reducePrincipal;
this.principalPortionMap = principalPortionMap;
this.latePaymentMap = latePaymentMap;
this.compoundingMap = compoundingMap;
this.unCompoundedAmount = unCompoundedAmount;
this.disburseDetailMap = disburseDetailMap;
this.principalToBeScheduled = principalToBeScheduled;
this.outstandingBalance = outstandingBalance;
this.outstandingBalanceAsPerRest = outstandingBalanceAsPerRest;
this.installments = installments;
this.recalculationDetails = recalculationDetails;
this.loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessor;
this.scheduleTillDate = scheduleTillDate;
this.partialUpdate = partialUpdate;
this.currency = currency;
this.applyInterestRecalculation = applyInterestRecalculation;
if (this.currency != null) {
this.compoundedInLastInstallment = Money.zero(this.currency);
}
}
public static LoanScheduleParams createLoanScheduleParamsForPartialUpdate(final int periodNumber, final int instalmentNumber,
int loanTermInDays, LocalDate periodStartDate, final LocalDate actualRepaymentDate, final Money totalCumulativePrincipal,
final Money totalCumulativeInterest, final Money totalFeeChargesCharged, final Money totalPenaltyChargesCharged,
final Money totalRepaymentExpected, Money totalOutstandingInterestPaymentDueToGrace, final Money reducePrincipal,
final Map<LocalDate, Money> principalPortionMap, final Map<LocalDate, Money> latePaymentMap,
final Map<LocalDate, Money> compoundingMap, Money unCompoundedAmount, final Map<LocalDate, Money> disburseDetailMap,
final Money principalToBeScheduled, final Money outstandingBalance, final Money outstandingBalanceAsPerRest,
final List<LoanRepaymentScheduleInstallment> installments, final Collection<RecalculationDetail> recalculationDetails,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate,
final MonetaryCurrency currency, final boolean applyInterestRecalculation) {
final boolean partialUpdate = true;
return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate,
totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
}
public static LoanScheduleParams createLoanScheduleParamsForCompleteUpdate(final Collection<RecalculationDetail> recalculationDetails,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate,
final boolean applyInterestRecalculation) {
final int periodNumber = 1;
final int instalmentNumber = 1;
final LocalDate periodStartDate = null;
final LocalDate actualRepaymentDate = null;
final Money totalCumulativePrincipal = null;
final Money totalCumulativeInterest = null;
final Money totalFeeChargesCharged = null;
final Money totalPenaltyChargesCharged = null;
final Money totalRepaymentExpected = null;
final Money reducePrincipal = null;
final Map<LocalDate, Money> principalPortionMap = null;
final Map<LocalDate, Money> latePaymentMap = null;
final Map<LocalDate, Money> compoundingMap = null;
final Map<LocalDate, Money> disburseDetailMap = null;
final Money principalToBeScheduled = null;
final Money outstandingBalance = null;
final Money outstandingBalanceAsPerRest = null;
final List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
final boolean partialUpdate = false;
final int loanTermInDays = 0;
final Money totalOutstandingInterestPaymentDueToGrace = null;
final MonetaryCurrency currency = null;
final Money unCompoundedAmount = null;
return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate,
totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
}
public static LoanScheduleParams createLoanScheduleParams(final MonetaryCurrency currency, final Money chargesDueAtTimeOfDisbursement,
final LocalDate periodStartDate, final Money principalToBeScheduled) {
final int loanTermInDays = 0;
final int periodNumber = 1;
final int instalmentNumber = 1;
final Money totalCumulativePrincipal = Money.zero(currency);
final Money totalCumulativeInterest = Money.zero(currency);
final Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency);
final LocalDate actualRepaymentDate = periodStartDate;
final Money totalFeeChargesCharged = chargesDueAtTimeOfDisbursement;
final Money totalPenaltyChargesCharged = Money.zero(currency);
final Money totalRepaymentExpected = chargesDueAtTimeOfDisbursement;
final Money reducePrincipal = Money.zero(currency);
final Map<LocalDate, Money> principalPortionMap = new HashMap<>();
final Map<LocalDate, Money> latePaymentMap = new HashMap<>();
final Map<LocalDate, Money> compoundingMap = new TreeMap<>();
final Map<LocalDate, Money> disburseDetailMap = new TreeMap<>();
final Money outstandingBalance = principalToBeScheduled;
final Money outstandingBalanceAsPerRest = principalToBeScheduled;
final List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
final boolean partialUpdate = false;
final Collection<RecalculationDetail> recalculationDetails = null;
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = null;
final LocalDate scheduleTillDate = null;
final Money unCompoundedAmount = Money.zero(currency);
final boolean applyInterestRecalculation = false;
return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate,
totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
}
public static LoanScheduleParams createLoanScheduleParams(final MonetaryCurrency currency, final Money chargesDueAtTimeOfDisbursement,
final LocalDate periodStartDate, final Money principalToBeScheduled, final LoanScheduleParams loanScheduleParams) {
final int loanTermInDays = 0;
final int periodNumber = 1;
final int instalmentNumber = 1;
final Money totalCumulativePrincipal = Money.zero(currency);
final Money totalCumulativeInterest = Money.zero(currency);
final Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency);
final LocalDate actualRepaymentDate = periodStartDate;
final Money totalFeeChargesCharged = chargesDueAtTimeOfDisbursement;
final Money totalPenaltyChargesCharged = Money.zero(currency);
final Money totalRepaymentExpected = chargesDueAtTimeOfDisbursement;
final Money reducePrincipal = Money.zero(currency);
final Map<LocalDate, Money> principalPortionMap = new HashMap<>();
final Map<LocalDate, Money> latePaymentMap = new HashMap<>();
final Map<LocalDate, Money> compoundingMap = new TreeMap<>();
final Map<LocalDate, Money> disburseDetailMap = new TreeMap<>();
final Money outstandingBalance = principalToBeScheduled;
final Money outstandingBalanceAsPerRest = principalToBeScheduled;
final List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
final boolean partialUpdate = false;
final Collection<RecalculationDetail> recalculationDetails = loanScheduleParams.recalculationDetails;
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanScheduleParams.loanRepaymentScheduleTransactionProcessor;
final LocalDate scheduleTillDate = loanScheduleParams.scheduleTillDate;
final boolean applyInterestRecalculation = loanScheduleParams.applyInterestRecalculation;
final Money unCompoundedAmount = Money.zero(currency);
return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate,
totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
}
public int getPeriodNumber() {
return this.periodNumber;
}
public void setPeriodNumber(int periodNumber) {
this.periodNumber = periodNumber;
}
public int getInstalmentNumber() {
return this.instalmentNumber;
}
public LocalDate getActualRepaymentDate() {
return this.actualRepaymentDate;
}
public void setActualRepaymentDate(LocalDate actualRepaymentDate) {
this.actualRepaymentDate = actualRepaymentDate;
}
public Money getTotalCumulativePrincipal() {
return this.totalCumulativePrincipal;
}
public void addTotalCumulativePrincipal(final Money totalCumulativePrincipal) {
if (this.totalCumulativePrincipal != null) {
this.totalCumulativePrincipal = this.totalCumulativePrincipal.plus(totalCumulativePrincipal);
} else {
this.totalCumulativePrincipal = totalCumulativePrincipal;
}
}
public Money getTotalCumulativeInterest() {
return this.totalCumulativeInterest;
}
public void addTotalCumulativeInterest(final Money totalCumulativeInterest) {
if (this.totalCumulativeInterest != null) {
this.totalCumulativeInterest = this.totalCumulativeInterest.plus(totalCumulativeInterest);
} else {
this.totalCumulativeInterest = totalCumulativeInterest;
}
}
public Money getTotalFeeChargesCharged() {
return this.totalFeeChargesCharged;
}
public void addTotalFeeChargesCharged(final Money totalFeeChargesCharged) {
if (this.totalFeeChargesCharged != null) {
this.totalFeeChargesCharged = this.totalFeeChargesCharged.plus(totalFeeChargesCharged);
} else {
this.totalFeeChargesCharged = totalFeeChargesCharged;
}
}
public Money getTotalPenaltyChargesCharged() {
return this.totalPenaltyChargesCharged;
}
public void addTotalPenaltyChargesCharged(final Money totalPenaltyChargesCharged) {
if (this.totalPenaltyChargesCharged != null) {
this.totalPenaltyChargesCharged = this.totalPenaltyChargesCharged.plus(totalPenaltyChargesCharged);
} else {
this.totalPenaltyChargesCharged = totalPenaltyChargesCharged;
}
}
public Money getTotalRepaymentExpected() {
return this.totalRepaymentExpected;
}
public void addTotalRepaymentExpected(final Money totalRepaymentExpected) {
if (this.totalRepaymentExpected != null) {
this.totalRepaymentExpected = this.totalRepaymentExpected.plus(totalRepaymentExpected);
} else {
this.totalRepaymentExpected = totalRepaymentExpected;
}
}
public Money getReducePrincipal() {
return this.reducePrincipal;
}
public void setReducePrincipal(Money reducePrincipal) {
this.reducePrincipal = reducePrincipal;
}
public Map<LocalDate, Money> getPrincipalPortionMap() {
return this.principalPortionMap;
}
public Map<LocalDate, Money> getLatePaymentMap() {
return this.latePaymentMap;
}
public Map<LocalDate, Money> getCompoundingMap() {
return this.compoundingMap;
}
public Map<LocalDate, Money> getDisburseDetailMap() {
return this.disburseDetailMap;
}
public Money getOutstandingBalance() {
return this.outstandingBalance;
}
public void setOutstandingBalance(Money outstandingBalance) {
this.outstandingBalance = outstandingBalance;
}
public Money getOutstandingBalanceAsPerRest() {
return this.outstandingBalanceAsPerRest;
}
public void setOutstandingBalanceAsPerRest(Money outstandingBalanceAsPerRest) {
this.outstandingBalanceAsPerRest = outstandingBalanceAsPerRest;
}
public List<LoanRepaymentScheduleInstallment> getInstallments() {
return this.installments;
}
public Collection<RecalculationDetail> getRecalculationDetails() {
return this.recalculationDetails;
}
public LoanRepaymentScheduleTransactionProcessor getLoanRepaymentScheduleTransactionProcessor() {
return this.loanRepaymentScheduleTransactionProcessor;
}
public LocalDate getScheduleTillDate() {
return this.scheduleTillDate;
}
public boolean isPartialUpdate() {
return this.partialUpdate;
}
public LocalDate getPeriodStartDate() {
return this.periodStartDate;
}
public void setPeriodStartDate(LocalDate periodStartDate) {
this.periodStartDate = periodStartDate;
}
public Money getPrincipalToBeScheduled() {
return this.principalToBeScheduled;
}
public void setPrincipalToBeScheduled(Money principalToBeScheduled) {
this.principalToBeScheduled = principalToBeScheduled;
}
public void incrementPeriodNumber() {
this.periodNumber++;
}
public void incrementInstalmentNumber() {
this.instalmentNumber++;
}
public void addReducePrincipal(Money reducePrincipal) {
if (this.reducePrincipal != null) {
this.reducePrincipal = this.reducePrincipal.plus(reducePrincipal);
} else {
this.reducePrincipal = reducePrincipal;
}
}
public void reduceReducePrincipal(Money reducePrincipal) {
if (this.reducePrincipal != null) {
this.reducePrincipal = this.reducePrincipal.minus(reducePrincipal);
}
}
public void addPrincipalToBeScheduled(Money principalToBeScheduled) {
if (this.principalToBeScheduled != null) {
this.principalToBeScheduled = this.principalToBeScheduled.plus(principalToBeScheduled);
} else {
this.principalToBeScheduled = principalToBeScheduled;
}
}
public void addOutstandingBalance(Money outstandingBalance) {
if (this.outstandingBalance != null) {
this.outstandingBalance = this.outstandingBalance.plus(outstandingBalance);
} else {
this.outstandingBalance = outstandingBalance;
}
}
public void reduceOutstandingBalance(Money outstandingBalance) {
if (this.outstandingBalance != null) {
this.outstandingBalance = this.outstandingBalance.minus(outstandingBalance);
}
}
public void addOutstandingBalanceAsPerRest(Money outstandingBalanceAsPerRest) {
if (this.outstandingBalanceAsPerRest != null) {
this.outstandingBalanceAsPerRest = this.outstandingBalanceAsPerRest.plus(outstandingBalanceAsPerRest);
} else {
this.outstandingBalanceAsPerRest = outstandingBalanceAsPerRest;
}
}
public void reduceOutstandingBalanceAsPerRest(Money outstandingBalanceAsPerRest) {
if (this.outstandingBalanceAsPerRest != null) {
this.outstandingBalanceAsPerRest = this.outstandingBalanceAsPerRest.minus(outstandingBalanceAsPerRest);
}
}
public int getLoanTermInDays() {
return this.loanTermInDays;
}
public void addLoanTermInDays(int loanTermInDays) {
this.loanTermInDays += loanTermInDays;
}
public Money getTotalOutstandingInterestPaymentDueToGrace() {
return this.totalOutstandingInterestPaymentDueToGrace;
}
public void setTotalOutstandingInterestPaymentDueToGrace(Money totalOutstandingInterestPaymentDueToGrace) {
this.totalOutstandingInterestPaymentDueToGrace = totalOutstandingInterestPaymentDueToGrace;
}
public Map<LocalDate, Map<LocalDate, Money>> getCompoundingDateVariations() {
return this.compoundingDateVariations;
}
public MonetaryCurrency getCurrency() {
return this.currency;
}
public boolean applyInterestRecalculation() {
return this.applyInterestRecalculation;
}
public Money getUnCompoundedAmount() {
return this.unCompoundedAmount;
}
public void setUnCompoundedAmount(Money unCompoundedAmount) {
this.unCompoundedAmount = unCompoundedAmount;
}
public void addUnCompoundedAmount(Money unCompoundedAmount) {
if (this.unCompoundedAmount != null) {
this.unCompoundedAmount = this.unCompoundedAmount.plus(unCompoundedAmount);
} else {
this.unCompoundedAmount = unCompoundedAmount;
}
}
public void minusUnCompoundedAmount(Money unCompoundedAmount) {
if (this.unCompoundedAmount != null) {
this.unCompoundedAmount = this.unCompoundedAmount.minus(unCompoundedAmount);
}
}
public boolean isFirstPeriod() {
return 1 == instalmentNumber;
}
}
@@ -0,0 +1,155 @@
/**
* 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.math.BigDecimal;
import java.time.LocalDate;
import java.util.Set;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
/**
* Domain representation of a Loan Schedule Disbursement Period (not used for persistence)
*/
public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleModelPeriod {
@SuppressWarnings("unused")
private final Integer periodNumber;
private final LocalDate disbursementDate;
private final Money principalDisbursed;
private final BigDecimal chargesDueAtTimeOfDisbursement;
private boolean isEMIFixedSpecificToInstallment = false;
public static LoanScheduleModelDisbursementPeriod disbursement(final LoanApplicationTerms loanApplicationTerms,
final BigDecimal chargesDueAtTimeOfDisbursement) {
return new LoanScheduleModelDisbursementPeriod(null, loanApplicationTerms.getExpectedDisbursementDate(),
loanApplicationTerms.getPrincipal(), chargesDueAtTimeOfDisbursement);
}
public static LoanScheduleModelDisbursementPeriod disbursement(final LocalDate disbursementDate, final Money principalDisbursed,
final BigDecimal chargesDueAtTimeOfDisbursement) {
return new LoanScheduleModelDisbursementPeriod(null, disbursementDate, principalDisbursed, chargesDueAtTimeOfDisbursement);
}
private LoanScheduleModelDisbursementPeriod(final Integer periodNumber, final LocalDate disbursementDate,
final Money principalDisbursed, final BigDecimal chargesDueAtTimeOfDisbursement) {
this.periodNumber = periodNumber;
this.disbursementDate = disbursementDate;
this.principalDisbursed = principalDisbursed;
this.chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement;
}
@Override
public LoanSchedulePeriodData toData() {
return LoanSchedulePeriodData.disbursementOnlyPeriod(this.disbursementDate, this.principalDisbursed.getAmount(),
this.chargesDueAtTimeOfDisbursement, false);
}
@Override
public boolean isRepaymentPeriod() {
return false;
}
@Override
public boolean isDownPaymentPeriod() {
return false;
}
@Override
public Integer periodNumber() {
return null;
}
@Override
public LocalDate periodFromDate() {
return null;
}
@Override
public LocalDate periodDueDate() {
return null;
}
@Override
public BigDecimal principalDue() {
return null;
}
@Override
public BigDecimal interestDue() {
return null;
}
@Override
public BigDecimal feeChargesDue() {
return null;
}
@Override
public BigDecimal penaltyChargesDue() {
return null;
}
@Override
public void addLoanCharges(@SuppressWarnings("unused") BigDecimal feeCharge, @SuppressWarnings("unused") BigDecimal penaltyCharge) {
return;
}
@Override
public boolean isRecalculatedInterestComponent() {
return false;
}
@Override
public void addPrincipalAmount(@SuppressWarnings("unused") Money principalDue) {
return;
}
@Override
public void addInterestAmount(@SuppressWarnings("unused") Money principalDue) {
return;
}
@Override
public Set<LoanInterestRecalcualtionAdditionalDetails> getLoanCompoundingDetails() {
return null;
}
@Override
public void setEMIFixedSpecificToInstallmentTrue() {
this.isEMIFixedSpecificToInstallment = true;
}
@Override
public boolean isEMIFixedSpecificToInstallment() {
return isEMIFixedSpecificToInstallment;
}
@Override
public BigDecimal rescheduleInterestPortion() {
return null;
}
@Override
public void setRescheduleInterestPortion(BigDecimal rescheduleInterestPortion) {
return;
}
}
@@ -0,0 +1,193 @@
/**
* 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.math.BigDecimal;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
/**
* Domain representation of a Loan Schedule Repayment Period (not used for persistence)
*/
@Getter
public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModelPeriod {
private final int periodNumber;
private final LocalDate fromDate;
private final LocalDate dueDate;
private Money principalDue;
private final Money outstandingLoanBalance;
private Money interestDue;
private Money feeChargesDue;
private Money penaltyChargesDue;
private Money totalDue;
private final boolean recalculatedInterestComponent;
private final Set<LoanInterestRecalcualtionAdditionalDetails> loanCompoundingDetails = new HashSet<>();
private boolean isEMIFixedSpecificToInstallment = false;
BigDecimal rescheduleInterestPortion;
public static LoanScheduleModelRepaymentPeriod repayment(final int periodNumber, final LocalDate startDate,
final LocalDate scheduledDueDate, final Money principalDue, final Money outstandingLoanBalance, final Money interestDue,
final Money feeChargesDue, final Money penaltyChargesDue, final Money totalDue, boolean recalculatedInterestComponent) {
return new LoanScheduleModelRepaymentPeriod(periodNumber, startDate, scheduledDueDate, principalDue, outstandingLoanBalance,
interestDue, feeChargesDue, penaltyChargesDue, totalDue, recalculatedInterestComponent);
}
public LoanScheduleModelRepaymentPeriod(final int periodNumber, final LocalDate fromDate, final LocalDate dueDate,
final Money principalDue, final Money outstandingLoanBalance, final Money interestDue, final Money feeChargesDue,
final Money penaltyChargesDue, final Money totalDue, final boolean recalculatedInterestComponent) {
this.periodNumber = periodNumber;
this.fromDate = fromDate;
this.dueDate = dueDate;
this.principalDue = principalDue;
this.outstandingLoanBalance = outstandingLoanBalance;
this.interestDue = interestDue;
this.feeChargesDue = feeChargesDue;
this.penaltyChargesDue = penaltyChargesDue;
this.totalDue = totalDue;
this.recalculatedInterestComponent = recalculatedInterestComponent;
}
@Override
public LoanSchedulePeriodData toData() {
return LoanSchedulePeriodData.repaymentOnlyPeriod(this.periodNumber, this.fromDate, this.dueDate, this.principalDue.getAmount(),
this.outstandingLoanBalance.getAmount(), this.interestDue.getAmount(), this.feeChargesDue.getAmount(),
this.penaltyChargesDue.getAmount(), this.totalDue.getAmount(), this.principalDue.plus(this.interestDue).getAmount());
}
@Override
public boolean isRepaymentPeriod() {
return true;
}
@Override
public boolean isDownPaymentPeriod() {
return false;
}
@Override
public Integer periodNumber() {
return this.periodNumber;
}
@Override
public LocalDate periodFromDate() {
return this.fromDate;
}
@Override
public LocalDate periodDueDate() {
return this.dueDate;
}
@Override
public BigDecimal principalDue() {
BigDecimal value = null;
if (this.principalDue != null) {
value = this.principalDue.getAmount();
}
return value;
}
@Override
public BigDecimal interestDue() {
BigDecimal value = null;
if (this.interestDue != null) {
value = this.interestDue.getAmount();
}
return value;
}
@Override
public BigDecimal feeChargesDue() {
BigDecimal value = null;
if (this.feeChargesDue != null) {
value = this.feeChargesDue.getAmount();
}
return value;
}
@Override
public BigDecimal penaltyChargesDue() {
BigDecimal value = null;
if (this.penaltyChargesDue != null) {
value = this.penaltyChargesDue.getAmount();
}
return value;
}
@Override
public void addLoanCharges(BigDecimal feeCharge, BigDecimal penaltyCharge) {
this.feeChargesDue = this.feeChargesDue.plus(feeCharge);
this.penaltyChargesDue = this.penaltyChargesDue.plus(penaltyCharge);
this.totalDue = this.totalDue.plus(feeCharge).plus(penaltyCharge);
}
@Override
public void addPrincipalAmount(final Money principalDue) {
this.principalDue = this.principalDue.plus(principalDue);
this.totalDue = this.totalDue.plus(principalDue);
}
@Override
public boolean isRecalculatedInterestComponent() {
return this.recalculatedInterestComponent;
}
@Override
public void addInterestAmount(Money interestDue) {
this.interestDue = this.interestDue.plus(interestDue);
this.totalDue = this.totalDue.plus(interestDue);
}
@Override
public Set<LoanInterestRecalcualtionAdditionalDetails> getLoanCompoundingDetails() {
return this.loanCompoundingDetails;
}
@Override
public boolean isEMIFixedSpecificToInstallment() {
return this.isEMIFixedSpecificToInstallment;
}
@Override
public void setEMIFixedSpecificToInstallmentTrue() {
this.isEMIFixedSpecificToInstallment = true;
}
@Override
public void setRescheduleInterestPortion(BigDecimal rescheduleInterestPortion) {
this.rescheduleInterestPortion = rescheduleInterestPortion;
}
@Override
public BigDecimal rescheduleInterestPortion() {
return this.rescheduleInterestPortion;
}
}
@@ -0,0 +1,27 @@
/**
* 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.time.LocalDate;
import java.util.List;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
public record LoanTermVariationParams(boolean skipPeriod, boolean recalculateAmounts, LocalDate scheduledDueDate,
List<LoanTermVariationsData> variationsData) {
}
@@ -0,0 +1,50 @@
/**
* 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.time.LocalDate;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public class RecalculationDetail {
private LocalDate transactionDate;
private boolean isProcessed;
private LoanTransaction transaction;
public RecalculationDetail(final LocalDate transactionDate, final LoanTransaction transaction) {
this.transactionDate = transactionDate;
this.transaction = transaction;
}
public LocalDate getTransactionDate() {
return this.transactionDate;
}
public LoanTransaction getTransaction() {
return this.transaction;
}
public boolean isProcessed() {
return this.isProcessed;
}
public void setProcessed(boolean isProcessed) {
this.isProcessed = isProcessed;
}
}
@@ -0,0 +1,160 @@
/**
* 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.math.BigDecimal;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
public class ScheduleCurrentPeriodParams {
Money earlyPaidAmount;
LoanScheduleModelPeriod lastInstallment;
boolean skipCurrentLoop;
Money interestForThisPeriod;
Money principalForThisPeriod;
Money feeChargesForInstallment;
Money penaltyChargesForInstallment;
// for adjusting outstandingBalances
Money reducedBalance;
boolean isEmiAmountChanged;
BigDecimal interestCalculationGraceOnRepaymentPeriodFraction;
ScheduleCurrentPeriodParams(final MonetaryCurrency currency) {
this(currency, BigDecimal.ZERO);
}
ScheduleCurrentPeriodParams(final MonetaryCurrency currency, BigDecimal interestCalculationGraceOnRepaymentPeriodFraction) {
this.earlyPaidAmount = Money.zero(currency);
this.lastInstallment = null;
this.skipCurrentLoop = false;
this.interestForThisPeriod = Money.zero(currency);
this.principalForThisPeriod = Money.zero(currency);
this.reducedBalance = Money.zero(currency);
this.feeChargesForInstallment = Money.zero(currency);
this.penaltyChargesForInstallment = Money.zero(currency);
this.isEmiAmountChanged = false;
this.interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction;
}
public Money getEarlyPaidAmount() {
return this.earlyPaidAmount;
}
public void plusEarlyPaidAmount(Money earlyPaidAmount) {
this.earlyPaidAmount = this.earlyPaidAmount.plus(earlyPaidAmount);
}
public void minusEarlyPaidAmount(Money earlyPaidAmount) {
this.earlyPaidAmount = this.earlyPaidAmount.minus(earlyPaidAmount);
}
public LoanScheduleModelPeriod getLastInstallment() {
return this.lastInstallment;
}
public void setLastInstallment(LoanScheduleModelPeriod lastInstallment) {
this.lastInstallment = lastInstallment;
}
public boolean isSkipCurrentLoop() {
return this.skipCurrentLoop;
}
public void setSkipCurrentLoop(boolean skipCurrentLoop) {
this.skipCurrentLoop = skipCurrentLoop;
}
public Money getInterestForThisPeriod() {
return this.interestForThisPeriod;
}
public void setInterestForThisPeriod(Money interestForThisPeriod) {
this.interestForThisPeriod = interestForThisPeriod;
}
public void minusInterestForThisPeriod(Money interestForThisPeriod) {
this.interestForThisPeriod = this.interestForThisPeriod.minus(interestForThisPeriod);
}
public Money getPrincipalForThisPeriod() {
return this.principalForThisPeriod;
}
public void setPrincipalForThisPeriod(Money principalForThisPeriod) {
this.principalForThisPeriod = principalForThisPeriod;
}
public void plusPrincipalForThisPeriod(Money principalForThisPeriod) {
this.principalForThisPeriod = this.principalForThisPeriod.plus(principalForThisPeriod);
}
public void minusPrincipalForThisPeriod(Money principalForThisPeriod) {
this.principalForThisPeriod = this.principalForThisPeriod.minus(principalForThisPeriod);
}
public Money getReducedBalance() {
return this.reducedBalance;
}
public void setReducedBalance(Money reducedBalance) {
this.reducedBalance = reducedBalance;
}
public Money getFeeChargesForInstallment() {
return this.feeChargesForInstallment;
}
public void setFeeChargesForInstallment(Money feeChargesForInstallment) {
this.feeChargesForInstallment = feeChargesForInstallment;
}
public void minusFeeChargesForInstallment(Money feeChargesForInstallment) {
this.feeChargesForInstallment = this.feeChargesForInstallment.minus(feeChargesForInstallment);
}
public Money getPenaltyChargesForInstallment() {
return this.penaltyChargesForInstallment;
}
public void setPenaltyChargesForInstallment(Money penaltyChargesForInstallment) {
this.penaltyChargesForInstallment = penaltyChargesForInstallment;
}
public void minusPenaltyChargesForInstallment(Money penaltyChargesForInstallment) {
this.penaltyChargesForInstallment = this.penaltyChargesForInstallment.minus(penaltyChargesForInstallment);
}
public Money fetchTotalAmountForPeriod() {
return this.principalForThisPeriod.plus(interestForThisPeriod).plus(feeChargesForInstallment).plus(penaltyChargesForInstallment);
}
public boolean isEmiAmountChanged() {
return this.isEmiAmountChanged;
}
public void setEmiAmountChanged(boolean isEmiAmountChanged) {
this.isEmiAmountChanged = isEmiAmountChanged;
}
public BigDecimal getInterestCalculationGraceOnRepaymentPeriodFraction() {
return this.interestCalculationGraceOnRepaymentPeriodFraction;
}
}
@@ -0,0 +1,52 @@
/**
* 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.time.LocalDate;
import org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
public interface ScheduledDateGenerator {
LocalDate getLastRepaymentDate(LoanApplicationTerms loanApplicationTerms, HolidayDetailDTO holidayDetailDTO);
LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType repaymentPeriodFrequencyType, int repaidEvery,
LocalDate firstRepaymentDate, Calendar loanCalendar, HolidayDetailDTO holidayDetailDTO,
LoanApplicationTerms loanApplicationTerms);
LocalDate generateNextRepaymentDate(LocalDate lastRepaymentDate, LoanApplicationTerms loanApplicationTerms, boolean isFirstRepayment);
LocalDate generateNextRepaymentDate(LocalDate lastRepaymentDate, LoanApplicationTerms loanApplicationTerms, boolean isFirstRepayment,
Integer periodNumber);
AdjustedDateDetailsDTO adjustRepaymentDate(LocalDate dueRepaymentPeriodDate, LoanApplicationTerms loanApplicationTerms,
HolidayDetailDTO holidayDetailDTO);
LocalDate getRepaymentPeriodDate(PeriodFrequencyType frequency, int repaidEvery, LocalDate startDate);
Boolean isDateFallsInSchedule(PeriodFrequencyType frequency, int repaidEvery, LocalDate startDate, LocalDate date);
LocalDate generateNextScheduleDateStartingFromDisburseDate(LocalDate lastRepaymentDate, LoanApplicationTerms loanApplicationTerms,
HolidayDetailDTO holidayDetailDTO);
LocalDate generateNextScheduleDateStartingFromDisburseDateOrRescheduleDate(LocalDate lastRepaymentDate,
LoanApplicationTerms loanApplicationTerms, HolidayDetailDTO holidayDetailDTO);
}
@@ -0,0 +1,29 @@
/**
* 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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class MultiDisbursementOutstandingAmoutException extends AbstractPlatformDomainRuleException {
public MultiDisbursementOutstandingAmoutException(final String defaultUserMessage, final Object... defaultUserMessageArgs) {
super("error.msg.loanschedule.exceeding.max.outstanding.balance", defaultUserMessage, defaultUserMessageArgs);
}
}
@@ -1,122 +0,0 @@
/**
* 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.loanproduct.domain;
import com.google.common.base.Enums;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
public class AdvancedPaymentAllocationsJsonParser {
public final AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator;
public List<LoanProductPaymentAllocationRule> assembleLoanProductPaymentAllocationRules(final JsonCommand command,
String loanTransactionProcessingStrategyCode) {
JsonArray paymentAllocations = command.arrayOfParameterNamed("paymentAllocation");
List<LoanProductPaymentAllocationRule> productPaymentAllocationRules = null;
if (paymentAllocations != null) {
productPaymentAllocationRules = paymentAllocations.asList().stream().map(json -> {
Map<String, JsonElement> map = json.getAsJsonObject().asMap();
LoanProductPaymentAllocationRule loanProductPaymentAllocationRule = new LoanProductPaymentAllocationRule();
populatePaymentAllocationRules(map, loanProductPaymentAllocationRule);
populateFutureInstallment(map, loanProductPaymentAllocationRule);
populateTransactionType(map, loanProductPaymentAllocationRule);
return loanProductPaymentAllocationRule;
}).toList();
}
advancedPaymentAllocationsValidator.validate(productPaymentAllocationRules, loanTransactionProcessingStrategyCode);
return productPaymentAllocationRules;
}
private void populatePaymentAllocationRules(Map<String, JsonElement> map,
LoanProductPaymentAllocationRule loanProductPaymentAllocationRule) {
JsonArray paymentAllocationOrder = asJsonArrayOrNull(map.get("paymentAllocationOrder"));
if (paymentAllocationOrder != null) {
loanProductPaymentAllocationRule.setAllocationTypes(getPaymentAllocationTypes(paymentAllocationOrder));
}
}
private void populateFutureInstallment(Map<String, JsonElement> map,
LoanProductPaymentAllocationRule loanProductPaymentAllocationRule) {
String futureInstallmentAllocationRule = asStringOrNull(map.get("futureInstallmentAllocationRule"));
if (futureInstallmentAllocationRule != null) {
loanProductPaymentAllocationRule.setFutureInstallmentAllocationRule(
Enums.getIfPresent(FutureInstallmentAllocationRule.class, futureInstallmentAllocationRule).orNull());
}
}
private void populateTransactionType(Map<String, JsonElement> map, LoanProductPaymentAllocationRule loanProductPaymentAllocationRule) {
String transactionType = asStringOrNull(map.get("transactionType"));
if (transactionType != null) {
loanProductPaymentAllocationRule
.setTransactionType(Enums.getIfPresent(PaymentAllocationTransactionType.class, transactionType).orNull());
}
}
@NotNull
private List<PaymentAllocationType> getPaymentAllocationTypes(JsonArray paymentAllocationOrder) {
if (paymentAllocationOrder != null) {
List<Pair<Integer, PaymentAllocationType>> parsedListWithOrder = paymentAllocationOrder.asList().stream().map(json -> {
Map<String, JsonElement> map = json.getAsJsonObject().asMap();
PaymentAllocationType paymentAllocationType = null;
String paymentAllocationRule = asStringOrNull(map.get("paymentAllocationRule"));
if (paymentAllocationRule != null) {
paymentAllocationType = Enums.getIfPresent(PaymentAllocationType.class, paymentAllocationRule).orNull();
}
return Pair.of(asIntegerOrNull(map.get("order")), paymentAllocationType);
}).sorted(Comparator.comparing(Pair::getLeft)).toList();
advancedPaymentAllocationsValidator.validatePairOfOrderAndPaymentAllocationType(parsedListWithOrder);
return parsedListWithOrder.stream().map(Pair::getRight).toList();
} else {
return List.of();
}
}
private Integer asIntegerOrNull(JsonElement element) {
if (!element.isJsonNull()) {
return element.getAsInt();
}
return null;
}
private String asStringOrNull(JsonElement element) {
if (!element.isJsonNull()) {
return element.getAsString();
}
return null;
}
private JsonArray asJsonArrayOrNull(JsonElement element) {
if (!element.isJsonNull()) {
return element.getAsJsonArray();
}
return null;
}
}
@@ -1,139 +0,0 @@
/**
* 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.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;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.springframework.stereotype.Service;
@Service
public class AdvancedPaymentAllocationsValidator {
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
public void validate(List<LoanProductPaymentAllocationRule> rules, String code) {
if (isAdvancedPaymentStrategy(code)) {
if (!hasLoanProductPaymentAllocationRule(rules) || !hasAtLeastOneDefaultPaymentAllocation(rules)) {
raiseValidationError("advanced-payment-strategy-without-default-payment-allocation",
"Advanced-payment-allocation-strategy was selected but no DEFAULT payment allocation was provided");
}
if (hasDuplicateTransactionTypes(rules)) {
raiseValidationError("advanced-payment-strategy-with-duplicate-payment-allocation",
"The same transaction type must be provided only once");
}
for (LoanProductPaymentAllocationRule rule : rules) {
validateAllocationRule(rule);
}
} else {
if (hasLoanProductPaymentAllocationRule(rules)) {
raiseValidationError("payment_allocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy",
"In case '" + code + "' payment strategy, payment_allocation must not be provided");
}
}
}
public void validatePairOfOrderAndPaymentAllocationType(List<Pair<Integer, PaymentAllocationType>> rules) {
if (rules.size() != 12) {
raiseValidationError("advanced-payment-strategy.each_payment_allocation_order.must.contain.12.entries",
"Each provided payment allocation must contain exactly 12 allocation rules, but " + rules.size() + " were provided");
}
List<PaymentAllocationType> deduped = rules.stream().map(Pair::getRight).distinct().toList();
if (deduped.size() != 12) {
raiseValidationError("advanced-payment-strategy.must.not.have.duplicate.payment.allocation.rule",
"The list of provided payment allocation rules must not contain any duplicates");
}
if (!Arrays.equals(IntStream.rangeClosed(1, 12).boxed().toArray(), rules.stream().map(Pair::getLeft).toArray())) {
raiseValidationError("advanced-payment-strategy.invalid.order", "The provided orders must be between 1 and 12");
}
}
public void checkGroupingOfAllocationRules(List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules) {
loanProductPaymentAllocationRules.forEach(paymentAllocationRule -> {
AtomicInteger pastDueRuleInteger = new AtomicInteger();
AtomicInteger dueRuleInteger = new AtomicInteger();
AtomicInteger inAdvanceRuleInteger = new AtomicInteger();
paymentAllocationRule.getAllocationTypes().forEach(paymentAllocationType -> {
validateAllocationType(paymentAllocationType, pastDueRuleInteger, dueRuleInteger, inAdvanceRuleInteger);
});
});
}
private void validateAllocationType(PaymentAllocationType paymentAllocationType, AtomicInteger pastDueRuleInteger,
AtomicInteger dueRuleInteger, AtomicInteger inAdvanceRuleInteger) {
switch (paymentAllocationType.getDueType()) {
case PAST_DUE -> validateDueType(pastDueRuleInteger, dueRuleInteger, inAdvanceRuleInteger);
case DUE -> validateDueType(dueRuleInteger, pastDueRuleInteger, inAdvanceRuleInteger);
case IN_ADVANCE -> validateDueType(inAdvanceRuleInteger, pastDueRuleInteger, dueRuleInteger);
}
}
private void validateDueType(AtomicInteger currentRuleInteger, AtomicInteger otherRule1Integer, AtomicInteger otherRule2Integer) {
currentRuleInteger.incrementAndGet();
if ((otherRule1Integer.get() > 0 && otherRule1Integer.get() < 4) || (otherRule2Integer.get() > 0 && otherRule2Integer.get() < 4)) {
raiseValidationError("mixed.due.type.allocation.rules.are.not.supported.with.horizontal.installment.processing",
"Horizontal repayment schedule processing is not supporting mixed due type allocation rules!");
}
}
private boolean hasDuplicateTransactionTypes(List<LoanProductPaymentAllocationRule> rules) {
return rules != null
&& rules.stream().map(LoanProductPaymentAllocationRule::getTransactionType).distinct().toList().size() != rules.size();
}
private void validateAllocationRule(LoanProductPaymentAllocationRule rule) {
if (rule.getTransactionType() == null) {
raiseValidationError("advanced-payment-strategy.with.not.valid.transaction.type",
"Payment allocation was provided with a not valid transaction type");
}
if (rule.getFutureInstallmentAllocationRule() == null) {
raiseValidationError("advanced-payment-strategy.with.not.valid.future.installment.allocation.rule",
"Payment allocation was provided without a valid future installment allocation rule");
}
}
private boolean isAdvancedPaymentStrategy(String code) {
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(code);
}
private boolean hasAtLeastOneDefaultPaymentAllocation(List<LoanProductPaymentAllocationRule> rules) {
return rules != null && rules.stream() //
.filter(r -> PaymentAllocationTransactionType.DEFAULT.equals(r.getTransactionType())) //
.toList() //
.size() > 0;
}
private boolean hasLoanProductPaymentAllocationRule(List<LoanProductPaymentAllocationRule> rules) {
return rules != null && rules.size() > 0;
}
private void raiseValidationError(String globalisationMessageCode, String msg) {
throw new PlatformApiDataValidationException(List.of(ApiParameterError.generalError(globalisationMessageCode, msg)));
}
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanproduct.domain;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanProduct classes
import java.util.Arrays;
import java.util.List;
import lombok.Getter;
@@ -1,110 +0,0 @@
/**
* 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.loanproduct.domain;
import com.google.common.base.Enums;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
public class CreditAllocationsJsonParser {
public final CreditAllocationsValidator creditAllocationsValidator;
public List<LoanProductCreditAllocationRule> assembleLoanProductCreditAllocationRules(final JsonCommand command,
String loanTransactionProcessingStrategyCode) {
JsonArray creditAllocation = command.arrayOfParameterNamed("creditAllocation");
List<LoanProductCreditAllocationRule> productCreditAllocationRules = null;
if (creditAllocation != null) {
productCreditAllocationRules = creditAllocation.asList().stream().map(json -> {
Map<String, JsonElement> map = json.getAsJsonObject().asMap();
LoanProductCreditAllocationRule creditAllocationRule = new LoanProductCreditAllocationRule();
populateCreditAllocationRules(map, creditAllocationRule);
populateTransactionType(map, creditAllocationRule);
return creditAllocationRule;
}).toList();
}
creditAllocationsValidator.validate(productCreditAllocationRules, loanTransactionProcessingStrategyCode);
return productCreditAllocationRules;
}
private void populateCreditAllocationRules(Map<String, JsonElement> map, LoanProductCreditAllocationRule creditAllocationRule) {
JsonArray creditAllocationOrder = asJsonArrayOrNull(map.get("creditAllocationOrder"));
if (creditAllocationOrder != null) {
creditAllocationRule.setAllocationTypes(getAllocationTypes(creditAllocationOrder));
}
}
private void populateTransactionType(Map<String, JsonElement> map, LoanProductCreditAllocationRule creditAllocationRule) {
String transactionType = asStringOrNull(map.get("transactionType"));
if (transactionType != null) {
creditAllocationRule.setTransactionType(Enums.getIfPresent(CreditAllocationTransactionType.class, transactionType).orNull());
}
}
@NotNull
private List<AllocationType> getAllocationTypes(JsonArray allocationOrder) {
if (allocationOrder != null) {
List<Pair<Integer, AllocationType>> parsedListWithOrder = allocationOrder.asList().stream().map(json -> {
Map<String, JsonElement> map = json.getAsJsonObject().asMap();
AllocationType allocationType = null;
String creditAllocationRule = asStringOrNull(map.get("creditAllocationRule"));
if (creditAllocationRule != null) {
allocationType = Enums.getIfPresent(AllocationType.class, creditAllocationRule).orNull();
}
return Pair.of(asIntegerOrNull(map.get("order")), allocationType);
}).sorted(Comparator.comparing(Pair::getLeft)).toList();
creditAllocationsValidator.validatePairOfOrderAndCreditAllocationType(parsedListWithOrder);
return parsedListWithOrder.stream().map(Pair::getRight).toList();
} else {
return List.of();
}
}
private Integer asIntegerOrNull(JsonElement element) {
if (!element.isJsonNull()) {
return element.getAsInt();
}
return null;
}
private String asStringOrNull(JsonElement element) {
if (!element.isJsonNull()) {
return element.getAsString();
}
return null;
}
private JsonArray asJsonArrayOrNull(JsonElement element) {
if (!element.isJsonNull()) {
return element.getAsJsonArray();
}
return null;
}
}
@@ -1,96 +0,0 @@
/**
* 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.loanproduct.domain;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.springframework.stereotype.Service;
@Service
public class CreditAllocationsValidator {
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
public void validate(List<LoanProductCreditAllocationRule> rules, String code) {
if (isAdvancedPaymentStrategy(code)) {
if (hasDuplicateTransactionTypes(rules)) {
raiseValidationError("advanced-payment-strategy-with-duplicate-credit-allocation",
"The same transaction type must be provided only once");
}
if (rules != null) {
for (LoanProductCreditAllocationRule rule : rules) {
validateAllocationRule(rule);
}
}
} else {
if (hasLoanProductCreditAllocationRule(rules)) {
raiseValidationError("credit_allocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy",
"In case '" + code + "' payment strategy, creditAllocation must not be provided");
}
}
}
public void validatePairOfOrderAndCreditAllocationType(List<Pair<Integer, AllocationType>> rules) {
if (rules.size() != 4) {
raiseValidationError("advanced-payment-strategy.each_credit_allocation_order.must.contain.4.entries",
"Each provided credit allocation must contain exactly 4 allocation rules, but " + rules.size() + " were provided");
}
List<AllocationType> deduped = rules.stream().map(Pair::getRight).distinct().toList();
if (deduped.size() != 4) {
raiseValidationError("advanced-payment-strategy.must.not.have.duplicate.credit.allocation.rule",
"The list of provided credit allocation rules must not contain any duplicates");
}
if (!Arrays.equals(IntStream.rangeClosed(1, 4).boxed().toArray(), rules.stream().map(Pair::getLeft).toArray())) {
raiseValidationError("advanced-payment-strategy.invalid.order", "The provided orders must be between 1 and 4");
}
}
private boolean hasDuplicateTransactionTypes(List<LoanProductCreditAllocationRule> rules) {
return rules != null
&& rules.stream().map(LoanProductCreditAllocationRule::getTransactionType).distinct().toList().size() != rules.size();
}
private void validateAllocationRule(LoanProductCreditAllocationRule rule) {
if (rule.getTransactionType() == null) {
raiseValidationError("advanced-payment-strategy.with.not.valid.transaction.type",
"Credit allocation was provided with a not valid transaction type");
}
}
private boolean isAdvancedPaymentStrategy(String code) {
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(code);
}
private boolean hasLoanProductCreditAllocationRule(List<LoanProductCreditAllocationRule> rules) {
return rules != null && rules.size() > 0;
}
private void raiseValidationError(String globalisationMessageCode, String msg) {
throw new PlatformApiDataValidationException(List.of(ApiParameterError.generalError(globalisationMessageCode, msg)));
}
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanproduct.domain;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanProduct classes
import java.util.Arrays;
import java.util.List;
import lombok.Getter;
@@ -96,9 +96,13 @@ public class LoanProduct extends AbstractPersistableCustom {
@Column(name = "loan_transaction_strategy_name")
private String transactionProcessingStrategyName;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after removing association
// from LoanProduct entity
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loanProduct", orphanRemoval = true, fetch = FetchType.EAGER)
private List<LoanProductPaymentAllocationRule> paymentAllocationRules = new ArrayList<>();
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after removing association
// from LoanProduct entity
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loanProduct", orphanRemoval = true, fetch = FetchType.EAGER)
private List<LoanProductCreditAllocationRule> creditAllocationRules = new ArrayList<>();
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanproduct.domain;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanProduct classes
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanproduct.domain;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanProduct classes
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanproduct.domain;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanProduct classes
import java.util.Arrays;
import java.util.List;
import lombok.Getter;
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanproduct.domain;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanProduct classes
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanproduct.domain;
// TODO FINERACT-1932-Fineract modularization: Move to fineract-progressive-loan module after refactor of Loan and LoanProduct classes
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.List;
@@ -1,237 +0,0 @@
/**
* 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.loanproduct.domain;
import static org.apache.fineract.portfolio.loanproduct.domain.AdvancedPaymentAllocationsValidator.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
import static org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule.NEXT_INSTALLMENT;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.DEFAULT;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType.IN_ADVANCE_PENALTY;
import static org.mockito.Mockito.times;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonParser;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class AdvancedPaymentAllocationsJsonParserTest {
@Mock
private AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator;
@InjectMocks
private AdvancedPaymentAllocationsJsonParser advancedPaymentAllocationsJsonParser;
private FromJsonHelper fromJsonHelper = new FromJsonHelper();
@Test
public void testEmptyJson() throws JsonProcessingException {
Map<String, Object> map = new HashMap<>();
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, "other-strategy");
// then
Assertions.assertNull(loanProductPaymentAllocationRules);
Mockito.verify(advancedPaymentAllocationsValidator, times(1)).validate(null, "other-strategy");
}
@Test
public void testNullAllocationRuleJson() throws JsonProcessingException {
Map<String, Object> map = new HashMap<>();
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, "advanced-payment-allocation-strategy");
// then
Assertions.assertNull(loanProductPaymentAllocationRules);
Mockito.verify(advancedPaymentAllocationsValidator, times(1)).validate(null, "advanced-payment-allocation-strategy");
}
@Test
public void testParseSinglePaymentAllocation() throws JsonProcessingException {
// given
Map<String, Object> map = new HashMap<>();
List<Map<String, Object>> paymentAllocations = new ArrayList<>();
map.put("paymentAllocation", paymentAllocations);
List<String> allocationRule = EnumSet.allOf(PaymentAllocationType.class).stream().map(Enum::name).toList();
paymentAllocations.add(createPaymentAllocationEntry("DEFAULT", "NEXT_INSTALLMENT", allocationRule));
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
// then
Assertions.assertEquals(1, loanProductPaymentAllocationRules.size());
Assertions.assertEquals(NEXT_INSTALLMENT, loanProductPaymentAllocationRules.get(0).getFutureInstallmentAllocationRule());
Assertions.assertEquals(DEFAULT, loanProductPaymentAllocationRules.get(0).getTransactionType());
Assertions.assertEquals(12, loanProductPaymentAllocationRules.get(0).getAllocationTypes().size());
Assertions.assertEquals(EnumSet.allOf(PaymentAllocationType.class).stream().toList(),
loanProductPaymentAllocationRules.get(0).getAllocationTypes());
Mockito.verify(advancedPaymentAllocationsValidator, times(1)).validate(loanProductPaymentAllocationRules,
ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
Mockito.verify(advancedPaymentAllocationsValidator, times(1))
.validatePairOfOrderAndPaymentAllocationType(createPaymentAllocationTypeList());
Mockito.verifyNoMoreInteractions(advancedPaymentAllocationsValidator);
}
@Test
public void testInvalidTransactionTypeAndFutureAllocation() throws JsonProcessingException {
// given
Map<String, Object> map = new HashMap<>();
List<Map<String, Object>> paymentAllocations = new ArrayList<>();
map.put("paymentAllocation", paymentAllocations);
List<String> allocationRule = EnumSet.allOf(PaymentAllocationType.class).stream().map(Enum::name).toList();
paymentAllocations.add(createPaymentAllocationEntry("INVALID", "INVALID", allocationRule));
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
// then
Assertions.assertEquals(1, loanProductPaymentAllocationRules.size());
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getFutureInstallmentAllocationRule());
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getTransactionType());
Assertions.assertEquals(12, loanProductPaymentAllocationRules.get(0).getAllocationTypes().size());
Assertions.assertEquals(EnumSet.allOf(PaymentAllocationType.class).stream().toList(),
loanProductPaymentAllocationRules.get(0).getAllocationTypes());
Mockito.verify(advancedPaymentAllocationsValidator, times(1)).validate(loanProductPaymentAllocationRules,
ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
Mockito.verify(advancedPaymentAllocationsValidator, times(1))
.validatePairOfOrderAndPaymentAllocationType(createPaymentAllocationTypeList());
Mockito.verifyNoMoreInteractions(advancedPaymentAllocationsValidator);
}
@Test
public void testInvalidAndNullAllocationRules() throws JsonProcessingException {
// given
Map<String, Object> map = new HashMap<>();
List<Map<String, Object>> paymentAllocations = new ArrayList<>();
map.put("paymentAllocation", paymentAllocations);
List<String> allocationRule = Arrays.asList(new String[] { "invalid", null, "IN_ADVANCE_PENALTY" });
paymentAllocations.add(createPaymentAllocationEntry("DEFAULT", "NEXT_INSTALLMENT", allocationRule));
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
// then
Assertions.assertEquals(1, loanProductPaymentAllocationRules.size());
Assertions.assertEquals(NEXT_INSTALLMENT, loanProductPaymentAllocationRules.get(0).getFutureInstallmentAllocationRule());
Assertions.assertEquals(DEFAULT, loanProductPaymentAllocationRules.get(0).getTransactionType());
Assertions.assertEquals(3, loanProductPaymentAllocationRules.get(0).getAllocationTypes().size());
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getAllocationTypes().get(0));
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getAllocationTypes().get(1));
Assertions.assertEquals(IN_ADVANCE_PENALTY, loanProductPaymentAllocationRules.get(0).getAllocationTypes().get(2));
Mockito.verify(advancedPaymentAllocationsValidator, times(1)).validate(loanProductPaymentAllocationRules,
ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
Mockito.verify(advancedPaymentAllocationsValidator, times(1))
.validatePairOfOrderAndPaymentAllocationType(List.of(Pair.of(1, null), Pair.of(2, null), Pair.of(3, IN_ADVANCE_PENALTY)));
Mockito.verifyNoMoreInteractions(advancedPaymentAllocationsValidator);
}
@Test
public void testNullTransactionTypeAndFutureAllocation() throws JsonProcessingException {
// given
Map<String, Object> map = new HashMap<>();
List<Map<String, Object>> paymentAllocations = new ArrayList<>();
map.put("paymentAllocation", paymentAllocations);
List<String> allocationRule = EnumSet.allOf(PaymentAllocationType.class).stream().map(Enum::name).toList();
paymentAllocations.add(createPaymentAllocationEntry(null, null, allocationRule));
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
// then
Assertions.assertEquals(1, loanProductPaymentAllocationRules.size());
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getFutureInstallmentAllocationRule());
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getTransactionType());
Assertions.assertEquals(12, loanProductPaymentAllocationRules.get(0).getAllocationTypes().size());
Assertions.assertEquals(EnumSet.allOf(PaymentAllocationType.class).stream().toList(),
loanProductPaymentAllocationRules.get(0).getAllocationTypes());
Mockito.verify(advancedPaymentAllocationsValidator, times(1)).validate(loanProductPaymentAllocationRules,
ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
Mockito.verify(advancedPaymentAllocationsValidator, times(1))
.validatePairOfOrderAndPaymentAllocationType(createPaymentAllocationTypeList());
Mockito.verifyNoMoreInteractions(advancedPaymentAllocationsValidator);
}
public Map<String, Object> createPaymentAllocationEntry(String transactionType, String futureInstallmentAllocation,
List<String> orderedRules) {
Map<String, Object> map = new HashMap<>();
map.put("transactionType", transactionType);
map.put("futureInstallmentAllocationRule", futureInstallmentAllocation);
List<Map<String, Object>> paymentAllocationOrder = new ArrayList<>();
map.put("paymentAllocationOrder", paymentAllocationOrder);
for (int i = 0; i < orderedRules.size(); i++) {
HashMap<String, Object> entry = new HashMap<>();
entry.put("paymentAllocationRule", orderedRules.get(i));
entry.put("order", i + 1);
paymentAllocationOrder.add(entry);
}
return map;
}
private static List<Pair<Integer, PaymentAllocationType>> createPaymentAllocationTypeList() {
AtomicInteger i = new AtomicInteger(1);
List<Pair<Integer, PaymentAllocationType>> list = EnumSet.allOf(PaymentAllocationType.class).stream()
.map(p -> Pair.of(i.getAndIncrement(), p)).toList();
return list;
}
@NotNull
private JsonCommand createJsonCommand(Map<String, Object> jsonMap) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonMap);
JsonCommand command = JsonCommand.from(json, JsonParser.parseString(json), fromJsonHelper, null, 1L, 2L, 3L, 4L, null, null, null,
null, null, null, null, null);
return command;
}
}
@@ -1,227 +0,0 @@
/**
* 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.loanproduct.domain;
import static org.apache.fineract.portfolio.loanproduct.domain.AdvancedPaymentAllocationsValidator.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
import static org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule.LAST_INSTALLMENT;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.DEFAULT;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.REPAYMENT;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
class AdvancedPaymentAllocationsValidatorTest {
private AdvancedPaymentAllocationsValidator underTest = new AdvancedPaymentAllocationsValidator();
@Test
public void testPaymentAllocationsHasNoError() {
underTest.validatePairOfOrderAndPaymentAllocationType(createPaymentAllocationTypeList());
}
@Test
public void testPaymentAllocationsValidationThrowsErrorWhenLessElement() {
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class,
() -> underTest.validatePairOfOrderAndPaymentAllocationType(createPaymentAllocationTypeList().subList(0, 11)));
assertPlatformException("Each provided payment allocation must contain exactly 12 allocation rules, but 11 were provided",
"advanced-payment-strategy.each_payment_allocation_order.must.contain.12.entries", validationException);
}
@Test
public void testPaymentAllocationsValidationThrowsErrorWhenWithDuplicate() {
ArrayList<Pair<Integer, PaymentAllocationType>> pairs = new ArrayList<>(createPaymentAllocationTypeList().subList(0, 11));
pairs.add(pairs.get(10));
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class,
() -> underTest.validatePairOfOrderAndPaymentAllocationType(pairs));
assertPlatformException("The list of provided payment allocation rules must not contain any duplicates",
"advanced-payment-strategy.must.not.have.duplicate.payment.allocation.rule", validationException);
}
@Test
public void testPaymentAllocationsValidationThrowsErrorWhenOrderIsNotInRange() {
List<Pair<Integer, PaymentAllocationType>> pairs = createPaymentAllocationTypeList().stream()
.map(p -> Pair.of(p.getLeft() + 1, p.getRight())).toList();
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class,
() -> underTest.validatePairOfOrderAndPaymentAllocationType(pairs));
assertPlatformException("The provided orders must be between 1 and 12", "advanced-payment-strategy.invalid.order",
validationException);
}
@Test
public void testValidateNoError() {
LoanProductPaymentAllocationRule lppr1 = createLoanProductAllocationRule1();
LoanProductPaymentAllocationRule lppr2 = createLoanProductAllocationRule2();
underTest.validate(List.of(lppr1, lppr2), ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
}
@Test
public void testValidateEmptyRuleList() {
assertPlatformValidationException(
"Advanced-payment-allocation-strategy was selected but no DEFAULT payment allocation was provided",
"advanced-payment-strategy-without-default-payment-allocation",
() -> underTest.validate(List.of(), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
@Test
public void testValidateMissingList() {
assertPlatformValidationException(
"Advanced-payment-allocation-strategy was selected but no DEFAULT payment allocation was provided",
"advanced-payment-strategy-without-default-payment-allocation",
() -> underTest.validate(null, ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
@Test
public void testValidatePaymentAllocationThrowsErrorWhenNoDefault() {
LoanProductPaymentAllocationRule lppr2 = createLoanProductAllocationRule2();
assertPlatformValidationException(
"Advanced-payment-allocation-strategy was selected but no DEFAULT payment allocation was provided",
"advanced-payment-strategy-without-default-payment-allocation",
() -> underTest.validate(List.of(lppr2), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
@Test
public void testValidateThrowsErrorWhenDuplicate() {
LoanProductPaymentAllocationRule lppr1 = createLoanProductAllocationRule1();
LoanProductPaymentAllocationRule lppr2 = createLoanProductAllocationRule2();
LoanProductPaymentAllocationRule lppr3 = createLoanProductAllocationRule2();
assertPlatformValidationException("The same transaction type must be provided only once",
"advanced-payment-strategy-with-duplicate-payment-allocation",
() -> underTest.validate(List.of(lppr1, lppr2, lppr3), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
@Test
public void testValidateThrowsErrorWhenPaymentAllocationProvidedWithOtherStrategy() {
LoanProductPaymentAllocationRule lppr1 = createLoanProductAllocationRule1();
LoanProductPaymentAllocationRule lppr2 = createLoanProductAllocationRule2();
assertPlatformValidationException("In case 'some-other-strategy' payment strategy, payment_allocation must not be provided",
"payment_allocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy",
() -> underTest.validate(List.of(lppr1, lppr2), "some-other-strategy"));
}
@Test
public void testValidateThrowsErrorWhenFutureInstallmentIsEmpty() {
LoanProductPaymentAllocationRule lppr1 = createLoanProductAllocationRule1();
lppr1.setFutureInstallmentAllocationRule(null);
assertPlatformValidationException("Payment allocation was provided without a valid future installment allocation rule",
"advanced-payment-strategy.with.not.valid.future.installment.allocation.rule",
() -> underTest.validate(List.of(lppr1), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
@Test
public void testValidateThrowsErrorWhenTransactionTypeEmpty() {
LoanProductPaymentAllocationRule lppr1 = createLoanProductAllocationRule1();
LoanProductPaymentAllocationRule lppr2 = createLoanProductAllocationRule1();
lppr2.setTransactionType(null);
assertPlatformValidationException("Payment allocation was provided with a not valid transaction type",
"advanced-payment-strategy.with.not.valid.transaction.type",
() -> underTest.validate(List.of(lppr1, lppr2), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
@Test
public void checkGroupingOfAllocationRulesGoodOrder() {
List<LoanProductPaymentAllocationRule> allocationRules1 = List.of(createLoanProductAllocationRule1());
Assertions.assertDoesNotThrow(() -> underTest.checkGroupingOfAllocationRules(allocationRules1));
List<LoanProductPaymentAllocationRule> allocationRules2 = List.of(createLoanProductAllocationRule4());
Assertions.assertDoesNotThrow(() -> underTest.checkGroupingOfAllocationRules(allocationRules2));
}
@Test
public void checkGroupingOfAllocationRulesWrongOrder() {
List<LoanProductPaymentAllocationRule> allocationRules = List.of(createLoanProductAllocationRule3());
assertPlatformValidationException("Horizontal repayment schedule processing is not supporting mixed due type allocation rules!",
"mixed.due.type.allocation.rules.are.not.supported.with.horizontal.installment.processing",
() -> underTest.checkGroupingOfAllocationRules(allocationRules));
}
private void assertPlatformValidationException(String message, String code, Executable executable) {
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, executable);
assertPlatformException(message, code, validationException);
}
@NotNull
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule1() {
LoanProductPaymentAllocationRule lppr1 = new LoanProductPaymentAllocationRule();
lppr1.setTransactionType(DEFAULT);
lppr1.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
lppr1.setAllocationTypes(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
return lppr1;
}
@NotNull
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule2() {
LoanProductPaymentAllocationRule lppr2 = new LoanProductPaymentAllocationRule();
lppr2.setTransactionType(REPAYMENT);
lppr2.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
ArrayList<PaymentAllocationType> allocationTypes = new ArrayList<>(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
Collections.shuffle(allocationTypes);
lppr2.setAllocationTypes(allocationTypes);
return lppr2;
}
@NotNull
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule3() {
LoanProductPaymentAllocationRule lppr = new LoanProductPaymentAllocationRule();
lppr.setTransactionType(REPAYMENT);
lppr.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
ArrayList<PaymentAllocationType> allocationTypes = new ArrayList<>(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
Collections.swap(allocationTypes, 0, 4);
Collections.swap(allocationTypes, 3, 8);
Collections.swap(allocationTypes, 7, 11);
lppr.setAllocationTypes(allocationTypes);
return lppr;
}
@NotNull
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule4() {
LoanProductPaymentAllocationRule lppr = new LoanProductPaymentAllocationRule();
lppr.setTransactionType(DEFAULT);
lppr.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
ArrayList<PaymentAllocationType> allocationTypes = new ArrayList<>(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
Collections.swap(allocationTypes, 0, 4);
Collections.swap(allocationTypes, 1, 5);
Collections.swap(allocationTypes, 2, 6);
Collections.swap(allocationTypes, 3, 7);
lppr.setAllocationTypes(allocationTypes);
return lppr;
}
@NotNull
private static List<Pair<Integer, PaymentAllocationType>> createPaymentAllocationTypeList() {
AtomicInteger i = new AtomicInteger(1);
return EnumSet.allOf(PaymentAllocationType.class).stream().map(p -> Pair.of(i.getAndIncrement(), p)).toList();
}
private void assertPlatformException(String expectedMessage, String expectedCode,
PlatformApiDataValidationException platformApiDataValidationException) {
Assertions.assertEquals(expectedMessage, platformApiDataValidationException.getErrors().get(0).getDefaultUserMessage());
Assertions.assertEquals(expectedCode, platformApiDataValidationException.getErrors().get(0).getUserMessageGlobalisationCode());
}
}
@@ -1,125 +0,0 @@
/**
* 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.loanproduct.domain;
import static org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationsValidator.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
import static org.mockito.Mockito.times;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonParser;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class CreditAllocationsJsonParserTest {
@InjectMocks
private CreditAllocationsJsonParser creditAllocationsJsonParser;
@Mock
private CreditAllocationsValidator creditAllocationsValidator;
private FromJsonHelper fromJsonHelper = new FromJsonHelper();
@Test
public void testEmptyJson() throws JsonProcessingException {
Map<String, Object> map = new HashMap<>();
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductCreditAllocationRule> loanProductCreditAllocationRules = creditAllocationsJsonParser
.assembleLoanProductCreditAllocationRules(command, "other-strategy");
// then
Assertions.assertNull(loanProductCreditAllocationRules);
Mockito.verify(creditAllocationsValidator, times(1)).validate(null, "other-strategy");
}
@Test
public void testParseSingleChargebackAllocation() throws JsonProcessingException {
// given
Map<String, Object> map = new HashMap<>();
List<Map<String, Object>> creditAllocations = new ArrayList<>();
map.put("creditAllocation", creditAllocations);
List<String> allocationRule = EnumSet.allOf(AllocationType.class).stream().map(Enum::name).toList();
creditAllocations.add(createCreditAllocationEntry("CHARGEBACK", allocationRule));
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductCreditAllocationRule> creditAllocationRules = creditAllocationsJsonParser
.assembleLoanProductCreditAllocationRules(command, ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
// then
Assertions.assertEquals(1, creditAllocationRules.size());
Assertions.assertEquals(CreditAllocationTransactionType.CHARGEBACK, creditAllocationRules.get(0).getTransactionType());
Assertions.assertEquals(4, creditAllocationRules.get(0).getAllocationTypes().size());
Assertions.assertEquals(EnumSet.allOf(AllocationType.class).stream().toList(), creditAllocationRules.get(0).getAllocationTypes());
Mockito.verify(creditAllocationsValidator, times(1)).validate(creditAllocationRules, ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
Mockito.verify(creditAllocationsValidator, times(1)).validatePairOfOrderAndCreditAllocationType(createAllocationTypeList());
Mockito.verifyNoMoreInteractions(creditAllocationsValidator);
}
private static List<Pair<Integer, AllocationType>> createAllocationTypeList() {
AtomicInteger i = new AtomicInteger(1);
List<Pair<Integer, AllocationType>> list = EnumSet.allOf(AllocationType.class).stream().map(p -> Pair.of(i.getAndIncrement(), p))
.toList();
return list;
}
public Map<String, Object> createCreditAllocationEntry(String transactionType, List<String> orderedRules) {
Map<String, Object> map = new HashMap<>();
map.put("transactionType", transactionType);
List<Map<String, Object>> creditAllocationOrder = new ArrayList<>();
map.put("creditAllocationOrder", creditAllocationOrder);
for (int i = 0; i < orderedRules.size(); i++) {
HashMap<String, Object> entry = new HashMap<>();
entry.put("creditAllocationRule", orderedRules.get(i));
entry.put("order", i + 1);
creditAllocationOrder.add(entry);
}
return map;
}
@NotNull
private JsonCommand createJsonCommand(Map<String, Object> jsonMap) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonMap);
JsonCommand command = JsonCommand.from(json, JsonParser.parseString(json), fromJsonHelper, null, 1L, 2L, 3L, 4L, null, null, null,
null, null, null, null, null);
return command;
}
}
@@ -1,132 +0,0 @@
/**
* 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.loanproduct.domain;
import static org.apache.fineract.portfolio.loanproduct.domain.AdvancedPaymentAllocationsValidator.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
import static org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType.CHARGEBACK;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
public class CreditAllocationsValidatorTest {
private CreditAllocationsValidator underTest = new CreditAllocationsValidator();
@Test
public void testCreditAllocationsHasNoError() {
underTest.validatePairOfOrderAndCreditAllocationType(createCreditAllocationTypeList());
}
@Test
public void testCreditAllocationsValidationThrowsErrorWhenLessElement() {
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class,
() -> underTest.validatePairOfOrderAndCreditAllocationType(createCreditAllocationTypeList().subList(0, 3)));
assertPlatformException("Each provided credit allocation must contain exactly 4 allocation rules, but 3 were provided",
"advanced-payment-strategy.each_credit_allocation_order.must.contain.4.entries", validationException);
}
@Test
public void testCreditAllocationsValidationThrowsErrorWhenWithDuplicate() {
ArrayList<Pair<Integer, AllocationType>> pairs = new ArrayList<>(createCreditAllocationTypeList().subList(0, 3));
pairs.add(pairs.get(2));
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class,
() -> underTest.validatePairOfOrderAndCreditAllocationType(pairs));
assertPlatformException("The list of provided credit allocation rules must not contain any duplicates",
"advanced-payment-strategy.must.not.have.duplicate.credit.allocation.rule", validationException);
}
@Test
public void testCreditAllocationsValidationThrowsErrorWhenOrderIsNotInRange() {
List<Pair<Integer, AllocationType>> pairs = createCreditAllocationTypeList().stream()
.map(p -> Pair.of(p.getLeft() + 1, p.getRight())).toList();
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class,
() -> underTest.validatePairOfOrderAndCreditAllocationType(pairs));
assertPlatformException("The provided orders must be between 1 and 4", "advanced-payment-strategy.invalid.order",
validationException);
}
@Test
public void testValidateThrowsErrorWhenPaymentAllocationProvidedWithOtherStrategy() {
assertPlatformValidationException("In case 'some-other-strategy' payment strategy, creditAllocation must not be provided",
"credit_allocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy",
() -> underTest.validate(List.of(createLoanProductCreditAllocationRule1()), "some-other-strategy"));
}
@Test
public void testValidateThrowsErrorWhenTransactionTypeEmpty() {
LoanProductCreditAllocationRule lpcar = createLoanProductCreditAllocationRule1();
lpcar.setTransactionType(null);
assertPlatformValidationException("Credit allocation was provided with a not valid transaction type",
"advanced-payment-strategy.with.not.valid.transaction.type",
() -> underTest.validate(List.of(lpcar), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
@Test
public void testValidateNoError() {
underTest.validate(List.of(createLoanProductCreditAllocationRule1()), ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
}
@Test
public void testValidateCreditAllocationIsOptional() {
underTest.validate(List.of(), ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
}
@Test
public void testValidateThrowsErrorWhenDuplicate() {
assertPlatformValidationException("The same transaction type must be provided only once",
"advanced-payment-strategy-with-duplicate-credit-allocation",
() -> underTest.validate(List.of(createLoanProductCreditAllocationRule1(), createLoanProductCreditAllocationRule1()),
ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
@NotNull
private static List<Pair<Integer, AllocationType>> createCreditAllocationTypeList() {
AtomicInteger i = new AtomicInteger(1);
return EnumSet.allOf(AllocationType.class).stream().map(p -> Pair.of(i.getAndIncrement(), p)).toList();
}
@NotNull
private static LoanProductCreditAllocationRule createLoanProductCreditAllocationRule1() {
LoanProductCreditAllocationRule lpcr1 = new LoanProductCreditAllocationRule();
lpcr1.setTransactionType(CHARGEBACK);
lpcr1.setAllocationTypes(EnumSet.allOf(AllocationType.class).stream().toList());
return lpcr1;
}
private void assertPlatformValidationException(String message, String code, Executable executable) {
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, executable);
assertPlatformException(message, code, validationException);
}
private void assertPlatformException(String expectedMessage, String expectedCode,
PlatformApiDataValidationException platformApiDataValidationException) {
Assertions.assertEquals(expectedMessage, platformApiDataValidationException.getErrors().get(0).getDefaultUserMessage());
Assertions.assertEquals(expectedCode, platformApiDataValidationException.getErrors().get(0).getUserMessageGlobalisationCode());
}
}