FINERACT-1932: Fineract modularization Fineract progressive loan module
This commit is contained in:
committed by
Adam Saghy
parent
f59ae64222
commit
53f4864980
+1358
File diff suppressed because it is too large
Load Diff
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
+604
@@ -0,0 +1,604 @@
|
||||
/**
|
||||
* 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.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO;
|
||||
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
|
||||
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
|
||||
|
||||
public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
|
||||
@Override
|
||||
public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
|
||||
final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO) {
|
||||
// TODO: handle interest calculation
|
||||
final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency();
|
||||
// generate list of proposed schedule due dates
|
||||
LocalDate loanEndDate = getScheduledDateGenerator().getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO);
|
||||
LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations()
|
||||
.fetchLoanTermDueDateVariationsData(loanEndDate);
|
||||
if (lastDueDateVariation != null) {
|
||||
loanEndDate = lastDueDateVariation.getDateValue();
|
||||
}
|
||||
loanApplicationTerms.updateLoanEndDate(loanEndDate);
|
||||
|
||||
// determine the total charges due at time of disbursement
|
||||
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges);
|
||||
|
||||
// setup variables for tracking important facts required for loan
|
||||
// schedule generation.
|
||||
|
||||
final MonetaryCurrency currency = loanApplicationTerms.getCurrency();
|
||||
LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType())
|
||||
? loanApplicationTerms.getExpectedDisbursementDate()
|
||||
: loanApplicationTerms.getSubmittedOnDate();
|
||||
|
||||
LoanScheduleParams scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency,
|
||||
Money.of(currency, chargesDueAtTimeOfDisbursement), periodStartDate, Money.zero(currency));
|
||||
|
||||
List<LoanScheduleModelPeriod> periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms, scheduleParams,
|
||||
chargesDueAtTimeOfDisbursement);
|
||||
|
||||
boolean isFirstRepayment = true;
|
||||
|
||||
// charges which depends on total loan interest will be added to this
|
||||
// set and handled separately after all installments generated
|
||||
final Set<LoanCharge> nonCompoundingCharges = separateTotalCompoundingPercentageCharges(loanCharges);
|
||||
boolean isNextRepaymentAvailable = true;
|
||||
|
||||
boolean thereIsDisbursementBeforeOrOnLoanEndDate = scheduleParams.getDisburseDetailMap().entrySet().stream()
|
||||
.anyMatch(d -> !d.getKey().isAfter(loanApplicationTerms.getLoanEndDate()));
|
||||
while (!scheduleParams.getOutstandingBalance().isZero() || thereIsDisbursementBeforeOrOnLoanEndDate) {
|
||||
scheduleParams.setActualRepaymentDate(getScheduledDateGenerator().generateNextRepaymentDate(
|
||||
scheduleParams.getActualRepaymentDate(), loanApplicationTerms, isFirstRepayment, scheduleParams.getPeriodNumber()));
|
||||
AdjustedDateDetailsDTO adjustedDateDetailsDTO = getScheduledDateGenerator()
|
||||
.adjustRepaymentDate(scheduleParams.getActualRepaymentDate(), loanApplicationTerms, holidayDetailDTO);
|
||||
scheduleParams.setActualRepaymentDate(adjustedDateDetailsDTO.getChangedActualRepaymentDate());
|
||||
isFirstRepayment = false;
|
||||
LocalDate scheduledDueDate = adjustedDateDetailsDTO.getChangedScheduleDate();
|
||||
|
||||
// Loan Schedule Exceptions that need to be applied for Loan Account
|
||||
LoanTermVariationParams termVariationParams = applyLoanTermVariations(loanApplicationTerms, scheduleParams, scheduledDueDate);
|
||||
scheduledDueDate = termVariationParams.scheduledDueDate();
|
||||
|
||||
// Updates total days in term
|
||||
scheduleParams
|
||||
.addLoanTermInDays(Math.toIntExact(ChronoUnit.DAYS.between(scheduleParams.getPeriodStartDate(), scheduledDueDate)));
|
||||
|
||||
ScheduleCurrentPeriodParams currentPeriodParams = new ScheduleCurrentPeriodParams(currency, BigDecimal.ZERO);
|
||||
|
||||
processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods, scheduledDueDate);
|
||||
|
||||
// 5 determine principal,interest of repayment period
|
||||
PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod(loanApplicationTerms,
|
||||
scheduleParams, mc);
|
||||
|
||||
// update cumulative fields for principal
|
||||
currentPeriodParams.setPrincipalForThisPeriod(principalInterestForThisPeriod.principal());
|
||||
updateOutstandingBalance(scheduleParams, currentPeriodParams);
|
||||
|
||||
if (scheduleParams.getOutstandingBalance().isLessThanZero() || !isNextRepaymentAvailable) {
|
||||
currentPeriodParams.plusPrincipalForThisPeriod(scheduleParams.getOutstandingBalance());
|
||||
scheduleParams.setOutstandingBalance(Money.zero(currency));
|
||||
}
|
||||
|
||||
if (!isNextRepaymentAvailable) {
|
||||
scheduleParams.getDisburseDetailMap().clear();
|
||||
}
|
||||
|
||||
// applies charges for the period
|
||||
applyChargesForCurrentPeriod(loanCharges, currency, scheduleParams, scheduledDueDate, currentPeriodParams, mc);
|
||||
|
||||
// sum up real totalInstallmentDue from components
|
||||
final Money totalInstallmentDue = currentPeriodParams.fetchTotalAmountForPeriod();
|
||||
|
||||
// if previous installment is last then add interest to same
|
||||
// installment
|
||||
if (currentPeriodParams.getLastInstallment() != null && currentPeriodParams.getPrincipalForThisPeriod().isZero()) {
|
||||
currentPeriodParams.getLastInstallment().addInterestAmount(currentPeriodParams.getInterestForThisPeriod());
|
||||
continue;
|
||||
}
|
||||
|
||||
// create repayment period from parts
|
||||
LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(scheduleParams.getInstalmentNumber(),
|
||||
scheduleParams.getPeriodStartDate(), scheduledDueDate, currentPeriodParams.getPrincipalForThisPeriod(),
|
||||
scheduleParams.getOutstandingBalance(), currentPeriodParams.getInterestForThisPeriod(),
|
||||
currentPeriodParams.getFeeChargesForInstallment(), currentPeriodParams.getPenaltyChargesForInstallment(),
|
||||
totalInstallmentDue, false);
|
||||
if (principalInterestForThisPeriod.getRescheduleInterestPortion() != null) {
|
||||
installment.setRescheduleInterestPortion(principalInterestForThisPeriod.getRescheduleInterestPortion().getAmount());
|
||||
}
|
||||
addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment);
|
||||
|
||||
if (loanApplicationTerms.getCurrentPeriodFixedEmiAmount() != null) {
|
||||
installment.setEMIFixedSpecificToInstallmentTrue();
|
||||
}
|
||||
|
||||
periods.add(installment);
|
||||
|
||||
// Updates principal paid map with effective date for reducing
|
||||
// the amount from outstanding balance(interest calculation)
|
||||
updateAmountsWithEffectiveDate(loanApplicationTerms, holidayDetailDTO, scheduleParams, scheduledDueDate, currentPeriodParams,
|
||||
installment);
|
||||
|
||||
// handle cumulative fields
|
||||
|
||||
scheduleParams.addTotalCumulativePrincipal(currentPeriodParams.getPrincipalForThisPeriod());
|
||||
scheduleParams.addTotalRepaymentExpected(totalInstallmentDue);
|
||||
scheduleParams.addTotalCumulativeInterest(currentPeriodParams.getInterestForThisPeriod());
|
||||
scheduleParams.setPeriodStartDate(scheduledDueDate);
|
||||
scheduleParams.incrementInstalmentNumber();
|
||||
scheduleParams.incrementPeriodNumber();
|
||||
// if (termVariationParams.isRecalculateAmounts()) {
|
||||
// loanApplicationTerms.setCurrentPeriodFixedEmiAmount(null);
|
||||
// loanApplicationTerms.setCurrentPeriodFixedPrincipalAmount(null);
|
||||
// adjustInstallmentOrPrincipalAmount(loanApplicationTerms, scheduleParams.getTotalCumulativePrincipal(),
|
||||
// scheduleParams.getPeriodNumber(), mc);
|
||||
// }
|
||||
thereIsDisbursementBeforeOrOnLoanEndDate = scheduleParams.getDisburseDetailMap().entrySet().stream()
|
||||
.anyMatch(d -> !d.getKey().isAfter(loanApplicationTerms.getLoanEndDate()));
|
||||
}
|
||||
|
||||
// If the disbursement happened after maturity date
|
||||
if (loanApplicationTerms.isMultiDisburseLoan()) {
|
||||
processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods,
|
||||
DateUtils.getBusinessLocalDate().plusDays(1));
|
||||
}
|
||||
|
||||
// determine fees and penalties for charges which depends on total
|
||||
// loan interest
|
||||
updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges, mc);
|
||||
|
||||
final BigDecimal totalPrincipalPaid = BigDecimal.ZERO;
|
||||
final BigDecimal totalOutstanding = BigDecimal.ZERO;
|
||||
|
||||
return LoanScheduleModel.from(periods, applicationCurrency, scheduleParams.getLoanTermInDays(),
|
||||
scheduleParams.getPrincipalToBeScheduled().plus(loanApplicationTerms.getDownPaymentAmount()),
|
||||
scheduleParams.getTotalCumulativePrincipal().plus(loanApplicationTerms.getDownPaymentAmount()).getAmount(),
|
||||
totalPrincipalPaid, scheduleParams.getTotalCumulativeInterest().getAmount(),
|
||||
scheduleParams.getTotalFeeChargesCharged().getAmount(), scheduleParams.getTotalPenaltyChargesCharged().getAmount(),
|
||||
scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan,
|
||||
HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
|
||||
LocalDate rescheduleFrom) {
|
||||
|
||||
LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO);
|
||||
return LoanScheduleDTO.from(null, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
|
||||
LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
|
||||
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Abstract methods
|
||||
public abstract ScheduledDateGenerator getScheduledDateGenerator();
|
||||
|
||||
public abstract PaymentPeriodsInOneYearCalculator getPaymentPeriodsInOneYearCalculator();
|
||||
|
||||
public abstract PrincipalInterest calculatePrincipalInterestComponentsForPeriod(LoanApplicationTerms loanApplicationTerms,
|
||||
LoanScheduleParams loanScheduleParams, MathContext mc);
|
||||
|
||||
// Private, internal methods
|
||||
private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set<LoanCharge> loanCharges) {
|
||||
BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO;
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isDueAtDisbursement()) {
|
||||
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount());
|
||||
}
|
||||
}
|
||||
return chargesDueAtTimeOfDisbursement;
|
||||
}
|
||||
|
||||
private List<LoanScheduleModelPeriod> createNewLoanScheduleListWithDisbursementDetails(final LoanApplicationTerms loanApplicationTerms,
|
||||
final LoanScheduleParams loanScheduleParams, final BigDecimal chargesDueAtTimeOfDisbursement) {
|
||||
List<LoanScheduleModelPeriod> periods = new ArrayList<>();
|
||||
|
||||
// In case of `disallowExpectedDisbursementDetails = true`, anyway at least 1 disbursement details must exist
|
||||
if (loanApplicationTerms.getDisbursementDatas().isEmpty()) {
|
||||
loanApplicationTerms.getDisbursementDatas()
|
||||
.add(new DisbursementData(1L, loanApplicationTerms.getExpectedDisbursementDate(),
|
||||
loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms.getPrincipal().getAmount(), null, null,
|
||||
null, null));
|
||||
}
|
||||
for (DisbursementData disbursementData : loanApplicationTerms.getDisbursementDatas()) {
|
||||
Money principalMoney = Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal());
|
||||
if (disbursementData.disbursementDate().equals(loanScheduleParams.getPeriodStartDate())) {
|
||||
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement(
|
||||
disbursementData.disbursementDate(), Money.of(loanScheduleParams.getCurrency(), disbursementData.getPrincipal()),
|
||||
chargesDueAtTimeOfDisbursement);
|
||||
periods.add(disbursementPeriod);
|
||||
loanScheduleParams.addOutstandingBalance(principalMoney);
|
||||
loanScheduleParams.addOutstandingBalanceAsPerRest(principalMoney);
|
||||
loanScheduleParams.addPrincipalToBeScheduled(principalMoney);
|
||||
loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(principalMoney));
|
||||
loanApplicationTerms.resetFixedEmiAmount();
|
||||
if (loanApplicationTerms.isDownPaymentEnabled()) {
|
||||
final LoanScheduleModelDownPaymentPeriod downPaymentPeriod = createDownPaymentPeriod(loanApplicationTerms,
|
||||
loanScheduleParams, loanApplicationTerms.getExpectedDisbursementDate(), disbursementData.getPrincipal());
|
||||
periods.add(downPaymentPeriod);
|
||||
}
|
||||
} else {
|
||||
Money disbursedAmount = loanScheduleParams.getDisburseDetailMap().getOrDefault(disbursementData.disbursementDate(),
|
||||
Money.zero(loanApplicationTerms.getCurrency()));
|
||||
loanScheduleParams.getDisburseDetailMap().put(disbursementData.disbursementDate(), disbursedAmount.add(principalMoney));
|
||||
}
|
||||
}
|
||||
|
||||
return periods;
|
||||
}
|
||||
|
||||
private LoanScheduleModelDownPaymentPeriod createDownPaymentPeriod(LoanApplicationTerms loanApplicationTerms,
|
||||
LoanScheduleParams scheduleParams, LocalDate date, BigDecimal periodBaseAmount) {
|
||||
Money downPaymentAmount = Money.of(loanApplicationTerms.getCurrency(),
|
||||
MathUtil.percentageOf(periodBaseAmount, loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19));
|
||||
if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) {
|
||||
downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount, loanApplicationTerms.getInstallmentAmountInMultiplesOf());
|
||||
}
|
||||
LoanScheduleModelDownPaymentPeriod installment = LoanScheduleModelDownPaymentPeriod.downPayment(
|
||||
scheduleParams.getInstalmentNumber(), date, downPaymentAmount,
|
||||
scheduleParams.getOutstandingBalance().minus(downPaymentAmount));
|
||||
|
||||
addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment);
|
||||
|
||||
scheduleParams.incrementInstalmentNumber();
|
||||
scheduleParams.addTotalRepaymentExpected(downPaymentAmount);
|
||||
scheduleParams.reduceOutstandingBalance(downPaymentAmount);
|
||||
scheduleParams.reduceOutstandingBalanceAsPerRest(downPaymentAmount);
|
||||
scheduleParams.setPrincipalToBeScheduled(scheduleParams.getPrincipalToBeScheduled().minus(downPaymentAmount));
|
||||
loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().minus(downPaymentAmount));
|
||||
loanApplicationTerms.resetFixedEmiAmount();
|
||||
return installment;
|
||||
}
|
||||
|
||||
private LoanRepaymentScheduleInstallment addLoanRepaymentScheduleInstallment(final List<LoanRepaymentScheduleInstallment> installments,
|
||||
final LoanScheduleModelPeriod scheduledLoanInstallment) {
|
||||
LoanRepaymentScheduleInstallment installment = null;
|
||||
if (scheduledLoanInstallment.isRepaymentPeriod() || scheduledLoanInstallment.isDownPaymentPeriod()) {
|
||||
installment = new LoanRepaymentScheduleInstallment(null, scheduledLoanInstallment.periodNumber(),
|
||||
scheduledLoanInstallment.periodFromDate(), scheduledLoanInstallment.periodDueDate(),
|
||||
scheduledLoanInstallment.principalDue(), scheduledLoanInstallment.interestDue(),
|
||||
scheduledLoanInstallment.feeChargesDue(), scheduledLoanInstallment.penaltyChargesDue(),
|
||||
scheduledLoanInstallment.isRecalculatedInterestComponent(), scheduledLoanInstallment.getLoanCompoundingDetails(),
|
||||
scheduledLoanInstallment.rescheduleInterestPortion(), scheduledLoanInstallment.isDownPaymentPeriod());
|
||||
installments.add(installment);
|
||||
}
|
||||
return installment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method add extra disbursement periods (if applicable) and update the schedule params
|
||||
*/
|
||||
private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement,
|
||||
LoanScheduleParams scheduleParams, final Collection<LoanScheduleModelPeriod> periods, final LocalDate scheduledDueDate) {
|
||||
Iterator<Map.Entry<LocalDate, Money>> iter = scheduleParams.getDisburseDetailMap().entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
Map.Entry<LocalDate, Money> disburseDetail = iter.next();
|
||||
if ((disburseDetail.getKey().isEqual(scheduleParams.getPeriodStartDate())
|
||||
|| disburseDetail.getKey().isAfter(scheduleParams.getPeriodStartDate()))
|
||||
&& disburseDetail.getKey().isBefore(scheduledDueDate)) {
|
||||
// validation check for amount not exceeds specified max
|
||||
// amount as per the configuration
|
||||
if (loanApplicationTerms.isMultiDisburseLoan() && loanApplicationTerms.getMaxOutstandingBalance() != null) {
|
||||
Money maxOutstandingBalance = loanApplicationTerms.getMaxOutstandingBalanceMoney();
|
||||
if (scheduleParams.getOutstandingBalance().plus(disburseDetail.getValue()).isGreaterThan(maxOutstandingBalance)) {
|
||||
String errorMsg = "Outstanding balance must not exceed the amount: " + maxOutstandingBalance;
|
||||
throw new MultiDisbursementOutstandingAmoutException(errorMsg, loanApplicationTerms.getMaxOutstandingBalance(),
|
||||
disburseDetail.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// creates and add disbursement detail to the repayments
|
||||
// period
|
||||
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod
|
||||
.disbursement(disburseDetail.getKey(), disburseDetail.getValue(), chargesDueAtTimeOfDisbursement);
|
||||
periods.add(disbursementPeriod);
|
||||
|
||||
BigDecimal downPaymentAmt = BigDecimal.ZERO;
|
||||
// updates actual outstanding balance with new
|
||||
// disbursement detail
|
||||
scheduleParams.addOutstandingBalance(disburseDetail.getValue());
|
||||
scheduleParams.addOutstandingBalanceAsPerRest(disburseDetail.getValue());
|
||||
scheduleParams.addPrincipalToBeScheduled(disburseDetail.getValue());
|
||||
loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(disburseDetail.getValue()));
|
||||
loanApplicationTerms.resetFixedEmiAmount();
|
||||
if (loanApplicationTerms.isDownPaymentEnabled()) {
|
||||
// get list of disbursements done on same day and create down payment periods
|
||||
List<DisbursementData> disbursementsOnSameDate = loanApplicationTerms.getDisbursementDatas().stream()
|
||||
.filter(disbursementData -> DateUtils.isEqual(disbursementData.disbursementDate(), disburseDetail.getKey()))
|
||||
.toList();
|
||||
for (DisbursementData disbursementData : disbursementsOnSameDate) {
|
||||
final LoanScheduleModelDownPaymentPeriod downPaymentPeriod = createDownPaymentPeriod(loanApplicationTerms,
|
||||
scheduleParams, disbursementData.disbursementDate(), disbursementData.getPrincipal());
|
||||
periods.add(downPaymentPeriod);
|
||||
downPaymentAmt = downPaymentAmt.add(downPaymentPeriod.principalDue());
|
||||
}
|
||||
}
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyChargesForCurrentPeriod(final Set<LoanCharge> loanCharges, final MonetaryCurrency currency,
|
||||
LoanScheduleParams scheduleParams, LocalDate scheduledDueDate, ScheduleCurrentPeriodParams currentPeriodParams,
|
||||
final MathContext mc) {
|
||||
PrincipalInterest principalInterest = new PrincipalInterest(currentPeriodParams.getPrincipalForThisPeriod(),
|
||||
currentPeriodParams.getInterestForThisPeriod(), null);
|
||||
currentPeriodParams.setFeeChargesForInstallment(cumulativeFeeChargesDueWithin(scheduleParams.getPeriodStartDate(), scheduledDueDate,
|
||||
loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(),
|
||||
scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod(), mc));
|
||||
currentPeriodParams.setPenaltyChargesForInstallment(cumulativePenaltyChargesDueWithin(scheduleParams.getPeriodStartDate(),
|
||||
scheduledDueDate, loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(),
|
||||
scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod(), mc));
|
||||
scheduleParams.addTotalFeeChargesCharged(currentPeriodParams.getFeeChargesForInstallment());
|
||||
scheduleParams.addTotalPenaltyChargesCharged(currentPeriodParams.getPenaltyChargesForInstallment());
|
||||
}
|
||||
|
||||
private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set<LoanCharge> loanCharges,
|
||||
final MonetaryCurrency monetaryCurrency, final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed,
|
||||
final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod,
|
||||
final MathContext mc) {
|
||||
Money cumulative = Money.zero(monetaryCurrency);
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (!loanCharge.isDueAtDisbursement() && loanCharge.isFeeCharge()) {
|
||||
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
|
||||
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
|
||||
}
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money getCumulativeAmountOfCharge(LocalDate periodStart, LocalDate periodEnd, PrincipalInterest principalInterestForThisPeriod,
|
||||
Money principalDisbursed, Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable,
|
||||
boolean isFirstPeriod, LoanCharge loanCharge, Money cumulative, MathContext mc) {
|
||||
boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd)
|
||||
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd);
|
||||
if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) {
|
||||
cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, cumulative, loanCharge, mc);
|
||||
} else if (loanCharge.isOverdueInstallmentCharge() && isDue && loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
cumulative = cumulative.plus(loanCharge.chargeAmount());
|
||||
} else if (isDue && loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
cumulative = calculateSpecificDueDateChargeWithPercentage(principalDisbursed, totalInterestChargedForFullLoanTerm, cumulative,
|
||||
loanCharge, mc);
|
||||
} else if (isDue) {
|
||||
cumulative = cumulative.plus(loanCharge.amount());
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd,
|
||||
final Set<LoanCharge> loanCharges, final MonetaryCurrency monetaryCurrency,
|
||||
final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed,
|
||||
final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod,
|
||||
final MathContext mc) {
|
||||
Money cumulative = Money.zero(monetaryCurrency);
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
|
||||
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
|
||||
}
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money calculateInstallmentCharge(final PrincipalInterest principalInterestForThisPeriod, Money cumulative,
|
||||
final LoanCharge loanCharge, final MathContext mc) {
|
||||
if (loanCharge.getChargeCalculation().isPercentageBased()) {
|
||||
BigDecimal amount = BigDecimal.ZERO;
|
||||
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
|
||||
amount = amount.add(principalInterestForThisPeriod.principal().getAmount())
|
||||
.add(principalInterestForThisPeriod.interest().getAmount());
|
||||
} else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) {
|
||||
amount = amount.add(principalInterestForThisPeriod.interest().getAmount());
|
||||
} else {
|
||||
amount = amount.add(principalInterestForThisPeriod.principal().getAmount());
|
||||
}
|
||||
BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100), mc);
|
||||
cumulative = cumulative.plus(loanChargeAmt);
|
||||
} else {
|
||||
cumulative = cumulative.plus(loanCharge.amountOrPercentage());
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private Money calculateSpecificDueDateChargeWithPercentage(final Money principalDisbursed,
|
||||
final Money totalInterestChargedForFullLoanTerm, Money cumulative, final LoanCharge loanCharge, final MathContext mc) {
|
||||
BigDecimal amount = BigDecimal.ZERO;
|
||||
if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) {
|
||||
amount = amount.add(principalDisbursed.getAmount()).add(totalInterestChargedForFullLoanTerm.getAmount());
|
||||
} else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) {
|
||||
amount = amount.add(totalInterestChargedForFullLoanTerm.getAmount());
|
||||
} else {
|
||||
amount = amount.add(principalDisbursed.getAmount());
|
||||
}
|
||||
BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100), mc);
|
||||
cumulative = cumulative.plus(loanChargeAmt);
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
private void updateAmountsWithEffectiveDate(final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO,
|
||||
LoanScheduleParams scheduleParams, LocalDate scheduledDueDate, ScheduleCurrentPeriodParams currentPeriodParams,
|
||||
LoanScheduleModelPeriod installment) {
|
||||
LocalDate amountApplicableDate = installment.periodDueDate();
|
||||
if (loanApplicationTerms.isInterestRecalculationEnabled()) {
|
||||
amountApplicableDate = getNextRestScheduleDate(installment.periodDueDate().minusDays(1), loanApplicationTerms,
|
||||
holidayDetailDTO);
|
||||
}
|
||||
updateMapWithAmount(scheduleParams.getPrincipalPortionMap(),
|
||||
currentPeriodParams.getPrincipalForThisPeriod().minus(currentPeriodParams.getReducedBalance()), amountApplicableDate);
|
||||
|
||||
// update outstanding balance for interest calculation
|
||||
updateOutstandingBalanceAsPerRest(scheduleParams, scheduledDueDate);
|
||||
}
|
||||
|
||||
private LocalDate getNextRestScheduleDate(LocalDate startDate, LoanApplicationTerms loanApplicationTerms,
|
||||
final HolidayDetailDTO holidayDetailDTO) {
|
||||
LocalDate nextScheduleDate;
|
||||
if (loanApplicationTerms.getRecalculationFrequencyType().isSameAsRepayment()) {
|
||||
nextScheduleDate = getScheduledDateGenerator().generateNextScheduleDateStartingFromDisburseDateOrRescheduleDate(startDate,
|
||||
loanApplicationTerms, holidayDetailDTO);
|
||||
} else {
|
||||
CalendarInstance calendarInstance = loanApplicationTerms.getRestCalendarInstance();
|
||||
nextScheduleDate = CalendarUtils.getNextScheduleDate(calendarInstance.getCalendar(), startDate);
|
||||
}
|
||||
|
||||
return nextScheduleDate;
|
||||
}
|
||||
|
||||
private void updateMapWithAmount(final Map<LocalDate, Money> map, final Money amount, final LocalDate amountApplicableDate) {
|
||||
Money principalPaid = amount;
|
||||
if (map.containsKey(amountApplicableDate)) {
|
||||
principalPaid = map.get(amountApplicableDate).plus(principalPaid);
|
||||
}
|
||||
map.put(amountApplicableDate, principalPaid);
|
||||
|
||||
}
|
||||
|
||||
private void updateOutstandingBalanceAsPerRest(final LoanScheduleParams scheduleParams, final LocalDate scheduledDueDate) {
|
||||
scheduleParams.setOutstandingBalanceAsPerRest(updateBalanceForInterestCalculation(scheduleParams.getPrincipalPortionMap(),
|
||||
scheduledDueDate, scheduleParams.getOutstandingBalanceAsPerRest(), false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies all the past date principal changes and apply them on outstanding balance for future calculations
|
||||
*/
|
||||
private Money updateBalanceForInterestCalculation(final Map<LocalDate, Money> principalPortionMap, final LocalDate scheduledDueDate,
|
||||
final Money outstandingBalanceAsPerRest, boolean addMapDetails) {
|
||||
List<LocalDate> removeFromPrincipalPortionMap = new ArrayList<>();
|
||||
Money outstandingBalance = outstandingBalanceAsPerRest;
|
||||
for (Map.Entry<LocalDate, Money> principal : principalPortionMap.entrySet()) {
|
||||
if (!principal.getKey().isAfter(scheduledDueDate)) {
|
||||
if (addMapDetails) {
|
||||
outstandingBalance = outstandingBalance.plus(principal.getValue());
|
||||
} else {
|
||||
outstandingBalance = outstandingBalance.minus(principal.getValue());
|
||||
}
|
||||
removeFromPrincipalPortionMap.add(principal.getKey());
|
||||
}
|
||||
}
|
||||
for (LocalDate date : removeFromPrincipalPortionMap) {
|
||||
principalPortionMap.remove(date);
|
||||
}
|
||||
return outstandingBalance;
|
||||
}
|
||||
|
||||
private void updatePeriodsWithCharges(final MonetaryCurrency currency, LoanScheduleParams scheduleParams,
|
||||
final Collection<LoanScheduleModelPeriod> periods, final Set<LoanCharge> nonCompoundingCharges, MathContext mc) {
|
||||
for (LoanScheduleModelPeriod loanScheduleModelPeriod : periods) {
|
||||
if (loanScheduleModelPeriod.isRepaymentPeriod()) {
|
||||
PrincipalInterest principalInterest = new PrincipalInterest(Money.of(currency, loanScheduleModelPeriod.principalDue()),
|
||||
Money.of(currency, loanScheduleModelPeriod.interestDue()), null);
|
||||
Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(loanScheduleModelPeriod.periodFromDate(),
|
||||
loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest,
|
||||
scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(),
|
||||
!loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod(), mc);
|
||||
Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(loanScheduleModelPeriod.periodFromDate(),
|
||||
loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest,
|
||||
scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(),
|
||||
!loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod(), mc);
|
||||
scheduleParams.addTotalFeeChargesCharged(feeChargesForInstallment);
|
||||
scheduleParams.addTotalPenaltyChargesCharged(penaltyChargesForInstallment);
|
||||
scheduleParams.addTotalRepaymentExpected(feeChargesForInstallment.plus(penaltyChargesForInstallment));
|
||||
loanScheduleModelPeriod.addLoanCharges(feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<LoanCharge> separateTotalCompoundingPercentageCharges(final Set<LoanCharge> loanCharges) {
|
||||
Set<LoanCharge> interestCharges = new HashSet<>();
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isSpecifiedDueDate() && (loanCharge.getChargeCalculation().isPercentageOfInterest()
|
||||
|| loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest())) {
|
||||
interestCharges.add(loanCharge);
|
||||
}
|
||||
}
|
||||
loanCharges.removeAll(interestCharges);
|
||||
return interestCharges;
|
||||
}
|
||||
|
||||
private void updateOutstandingBalance(LoanScheduleParams scheduleParams, ScheduleCurrentPeriodParams currentPeriodParams) {
|
||||
// update outstandingLoanBlance using current period
|
||||
// 'principalDue'
|
||||
scheduleParams
|
||||
.reduceOutstandingBalance(currentPeriodParams.getPrincipalForThisPeriod().minus(currentPeriodParams.getReducedBalance()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loanApplicationTerms
|
||||
* @param scheduleParams
|
||||
* @param scheduledDueDate
|
||||
* @return
|
||||
*/
|
||||
private LoanTermVariationParams applyLoanTermVariations(final LoanApplicationTerms loanApplicationTerms,
|
||||
final LoanScheduleParams scheduleParams, final LocalDate scheduledDueDate) {
|
||||
boolean skipPeriod = false;
|
||||
boolean recalculateAmounts = false;
|
||||
LocalDate modifiedScheduledDueDate = scheduledDueDate;
|
||||
ArrayList<LoanTermVariationsData> variationsData = null;
|
||||
|
||||
// due date changes should be applied only for that dueDate
|
||||
if (loanApplicationTerms.getLoanTermVariations().hasDueDateVariation(scheduledDueDate)) {
|
||||
LoanTermVariationsData loanTermVariationsData = loanApplicationTerms.getLoanTermVariations().nextDueDateVariation();
|
||||
if (DateUtils.isEqual(modifiedScheduledDueDate, loanTermVariationsData.getTermVariationApplicableFrom())) {
|
||||
modifiedScheduledDueDate = loanTermVariationsData.getDateValue();
|
||||
loanApplicationTerms.updateVariationDays(DateUtils.getDifferenceInDays(scheduledDueDate, modifiedScheduledDueDate));
|
||||
if (!loanTermVariationsData.isSpecificToInstallment()) {
|
||||
scheduleParams.setActualRepaymentDate(modifiedScheduledDueDate);
|
||||
loanApplicationTerms.setNewScheduledDueDateStart(modifiedScheduledDueDate);
|
||||
}
|
||||
loanTermVariationsData.setProcessed(true);
|
||||
}
|
||||
}
|
||||
|
||||
return new LoanTermVariationParams(skipPeriod, recalculateAmounts, modifiedScheduledDueDate, variationsData);
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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.math.MathContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ProgressiveLoanScheduleGenerator extends AbstractProgressiveLoanScheduleGenerator {
|
||||
|
||||
private final ScheduledDateGenerator scheduledDateGenerator;
|
||||
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator;
|
||||
|
||||
@Override
|
||||
public ScheduledDateGenerator getScheduledDateGenerator() {
|
||||
return scheduledDateGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentPeriodsInOneYearCalculator getPaymentPeriodsInOneYearCalculator() {
|
||||
return paymentPeriodsInOneYearCalculator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(LoanApplicationTerms loanApplicationTerms,
|
||||
LoanScheduleParams loanScheduleParams, MathContext mc) {
|
||||
// TODO: handle interest calculation
|
||||
int periodNumber = loanScheduleParams.getPeriodNumber();
|
||||
BigDecimal fixedEMIAmount = loanApplicationTerms.getFixedEmiAmount();
|
||||
Money calculatedPrincipal;
|
||||
if (fixedEMIAmount == null) {
|
||||
int noRemainingPeriod = loanApplicationTerms.getActualNoOfRepaymnets() - periodNumber + 1;
|
||||
calculatedPrincipal = loanScheduleParams.getOutstandingBalance().dividedBy(noRemainingPeriod, mc.getRoundingMode());
|
||||
if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) {
|
||||
calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal,
|
||||
loanApplicationTerms.getInstallmentAmountInMultiplesOf());
|
||||
}
|
||||
loanApplicationTerms.setFixedEmiAmount(calculatedPrincipal.getAmount());
|
||||
} else {
|
||||
calculatedPrincipal = Money.of(loanApplicationTerms.getCurrency(), fixedEMIAmount);
|
||||
}
|
||||
// adjust if needed
|
||||
if (periodNumber == loanApplicationTerms.getActualNoOfRepaymnets()) {
|
||||
Money remainingAmount = loanScheduleParams.getOutstandingBalance().minus(calculatedPrincipal);
|
||||
calculatedPrincipal = calculatedPrincipal.plus(remainingAmount);
|
||||
}
|
||||
|
||||
return new PrincipalInterest(calculatedPrincipal, Money.zero(loanApplicationTerms.getCurrency()),
|
||||
Money.zero(loanApplicationTerms.getCurrency()));
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.starter;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
|
||||
public class AdvancedPaymentScheduleTransactionProcessorCondition extends PropertiesCondition {
|
||||
|
||||
@Override
|
||||
protected boolean matches(FineractProperties properties) {
|
||||
return properties.getLoan().getTransactionProcessor().getAdvancedPaymentStrategy().isEnabled();
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class AdvancedPaymentData implements Serializable {
|
||||
|
||||
private final String transactionType;
|
||||
private final String futureInstallmentAllocationRule;
|
||||
private final List<PaymentAllocationOrder> paymentAllocationOrder;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class PaymentAllocationOrder implements Serializable {
|
||||
|
||||
private final String paymentAllocationRule;
|
||||
private final Integer order;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class CreditAllocationData implements Serializable {
|
||||
|
||||
private final String transactionType;
|
||||
private final List<CreditAllocationOrder> creditAllocationOrder;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class CreditAllocationOrder implements Serializable {
|
||||
|
||||
private final String creditAllocationRule;
|
||||
private final Integer order;
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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)));
|
||||
}
|
||||
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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)));
|
||||
}
|
||||
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
@Mapper(config = MapstructMapperConfig.class)
|
||||
public interface AdvancedPaymentDataMapper {
|
||||
|
||||
List<AdvancedPaymentData> mapLoanProductPaymentAllocationRule(List<LoanProductPaymentAllocationRule> paymentAllocationRule);
|
||||
|
||||
List<AdvancedPaymentData> mapLoanPaymentAllocationRule(List<LoanPaymentAllocationRule> paymentAllocationRule);
|
||||
|
||||
@Mapping(target = "paymentAllocationOrder", source = "allocationTypes")
|
||||
AdvancedPaymentData mapLoanProductPaymentAllocationRule(LoanProductPaymentAllocationRule paymentAllocationRule);
|
||||
|
||||
@Mapping(target = "paymentAllocationOrder", source = "allocationTypes")
|
||||
AdvancedPaymentData mapLoanPaymentAllocationRule(LoanPaymentAllocationRule paymentAllocationRule);
|
||||
|
||||
default List<AdvancedPaymentData.PaymentAllocationOrder> mapAllocationTypes(List<PaymentAllocationType> allocationTypes) {
|
||||
AtomicInteger counter = new AtomicInteger(1);
|
||||
return allocationTypes.stream().map(a -> new AdvancedPaymentData.PaymentAllocationOrder(a.name(), counter.getAndIncrement()))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductCreditAllocationRule;
|
||||
|
||||
public class LoanProductCreditAllocationRuleMerger {
|
||||
|
||||
public boolean updateCreditAllocationRules(LoanProduct loanProduct,
|
||||
final List<LoanProductCreditAllocationRule> newLoanProductCreditAllocationRules) {
|
||||
if (newLoanProductCreditAllocationRules == null) {
|
||||
return false;
|
||||
}
|
||||
boolean updated = false;
|
||||
Map<CreditAllocationTransactionType, LoanProductCreditAllocationRule> originalItems = loanProduct.getCreditAllocationRules()
|
||||
.stream().collect(Collectors.toMap(LoanProductCreditAllocationRule::getTransactionType, Function.identity()));
|
||||
Map<CreditAllocationTransactionType, LoanProductCreditAllocationRule> newItems = newLoanProductCreditAllocationRules.stream()
|
||||
.collect(Collectors.toMap(LoanProductCreditAllocationRule::getTransactionType, Function.identity()));
|
||||
|
||||
// elements to be deleted
|
||||
Set<CreditAllocationTransactionType> existing = new HashSet<>(originalItems.keySet());
|
||||
Set<CreditAllocationTransactionType> newSet = new HashSet<>(newItems.keySet());
|
||||
existing.removeAll(newSet);
|
||||
if (existing.size() > 0) {
|
||||
updated = true;
|
||||
existing.forEach(type -> {
|
||||
loanProduct.getCreditAllocationRules().remove(originalItems.get(type));
|
||||
});
|
||||
}
|
||||
|
||||
// elements to be added
|
||||
existing = new HashSet<>(originalItems.keySet());
|
||||
newSet = new HashSet<>(newItems.keySet());
|
||||
newSet.removeAll(existing);
|
||||
if (newSet.size() > 0) {
|
||||
updated = true;
|
||||
newSet.forEach(type -> {
|
||||
loanProduct.getCreditAllocationRules().add(newItems.get(type));
|
||||
});
|
||||
}
|
||||
|
||||
// elements to be merged
|
||||
existing = new HashSet<>(originalItems.keySet());
|
||||
newSet = new HashSet<>(newItems.keySet());
|
||||
existing.retainAll(newSet);
|
||||
|
||||
for (CreditAllocationTransactionType type : existing) {
|
||||
boolean result = mergeLoanProductCreditAllocationRule(originalItems.get(type), newItems.get(type));
|
||||
if (result) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private boolean mergeLoanProductCreditAllocationRule(LoanProductCreditAllocationRule into, LoanProductCreditAllocationRule newElement) {
|
||||
boolean changed = false;
|
||||
|
||||
if (!Objects.equals(into.getAllocationTypes(), newElement.getAllocationTypes())) {
|
||||
into.setAllocationTypes(newElement.getAllocationTypes());
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
|
||||
|
||||
public class LoanProductPaymentAllocationRuleMerger {
|
||||
|
||||
public boolean updateProductPaymentAllocationRules(LoanProduct loanProduct,
|
||||
final List<LoanProductPaymentAllocationRule> newLoanProductPaymentAllocationRules) {
|
||||
if (newLoanProductPaymentAllocationRules == null) {
|
||||
return false;
|
||||
}
|
||||
boolean updated = false;
|
||||
Map<PaymentAllocationTransactionType, LoanProductPaymentAllocationRule> originalItems = loanProduct.getPaymentAllocationRules()
|
||||
.stream().collect(Collectors.toMap(LoanProductPaymentAllocationRule::getTransactionType, Function.identity()));
|
||||
Map<PaymentAllocationTransactionType, LoanProductPaymentAllocationRule> newItems = newLoanProductPaymentAllocationRules.stream()
|
||||
.collect(Collectors.toMap(LoanProductPaymentAllocationRule::getTransactionType, Function.identity()));
|
||||
|
||||
// elements to be deleted
|
||||
Set<PaymentAllocationTransactionType> existing = new HashSet<>(originalItems.keySet());
|
||||
Set<PaymentAllocationTransactionType> newSet = new HashSet<>(newItems.keySet());
|
||||
existing.removeAll(newSet);
|
||||
if (existing.size() > 0) {
|
||||
updated = true;
|
||||
existing.forEach(type -> {
|
||||
loanProduct.getPaymentAllocationRules().remove(originalItems.get(type));
|
||||
});
|
||||
}
|
||||
|
||||
// elements to be added
|
||||
existing = new HashSet<>(originalItems.keySet());
|
||||
newSet = new HashSet<>(newItems.keySet());
|
||||
newSet.removeAll(existing);
|
||||
if (newSet.size() > 0) {
|
||||
updated = true;
|
||||
newSet.forEach(type -> {
|
||||
loanProduct.getPaymentAllocationRules().add(newItems.get(type));
|
||||
});
|
||||
}
|
||||
|
||||
// elements to be merged
|
||||
existing = new HashSet<>(originalItems.keySet());
|
||||
newSet = new HashSet<>(newItems.keySet());
|
||||
existing.retainAll(newSet);
|
||||
|
||||
for (PaymentAllocationTransactionType type : existing) {
|
||||
boolean result = mergeLoanProductPaymentAllocationRule(originalItems.get(type), newItems.get(type));
|
||||
if (result) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private boolean mergeLoanProductPaymentAllocationRule(LoanProductPaymentAllocationRule into,
|
||||
LoanProductPaymentAllocationRule newElement) {
|
||||
boolean changed = false;
|
||||
|
||||
if (!Objects.equals(into.getFutureInstallmentAllocationRule(), newElement.getFutureInstallmentAllocationRule())) {
|
||||
into.setFutureInstallmentAllocationRule(newElement.getFutureInstallmentAllocationRule());
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!Objects.equals(into.getAllocationTypes(), newElement.getAllocationTypes())) {
|
||||
into.setAllocationTypes(newElement.getAllocationTypes());
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<!-- Sequence is starting from 5000 to make it easier to move existing liquibase changesets here -->
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
|
||||
|
||||
<persistence version="2.0"
|
||||
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
|
||||
|
||||
<!-- This file is only used for static weaving, nothing more. -->
|
||||
<!-- You can find the runtime configuration in the JPAConfig class -->
|
||||
<persistence-unit name="jpa-pu" transaction-type="RESOURCE_LOCAL">
|
||||
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
|
||||
<!-- Fineract core module -->
|
||||
<class>org.apache.fineract.accounting.glaccount.domain.GLAccount</class>
|
||||
<class>org.apache.fineract.accounting.journalentry.domain.JournalEntry</class>
|
||||
<class>org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom</class>
|
||||
<class>org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom</class>
|
||||
<class>org.apache.fineract.infrastructure.codes.domain.Code</class>
|
||||
<class>org.apache.fineract.infrastructure.codes.domain.CodeValue</class>
|
||||
<class>org.apache.fineract.infrastructure.documentmanagement.domain.Image</class>
|
||||
<class>org.apache.fineract.organisation.staff.domain.Staff</class>
|
||||
<class>org.apache.fineract.organisation.office.domain.Office</class>
|
||||
<class>org.apache.fineract.organisation.office.domain.OrganisationCurrency</class>
|
||||
<class>org.apache.fineract.organisation.monetary.domain.ApplicationCurrency</class>
|
||||
<class>org.apache.fineract.organisation.holiday.domain.Holiday</class>
|
||||
<class>org.apache.fineract.organisation.workingdays.domain.WorkingDays</class>
|
||||
<class>org.apache.fineract.portfolio.group.domain.Group</class>
|
||||
<class>org.apache.fineract.portfolio.group.domain.GroupLevel</class>
|
||||
<class>org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory</class>
|
||||
<class>org.apache.fineract.portfolio.group.domain.GroupRole</class>
|
||||
<class>org.apache.fineract.portfolio.client.domain.Client</class>
|
||||
<class>org.apache.fineract.portfolio.client.domain.ClientIdentifier</class>
|
||||
<class>org.apache.fineract.portfolio.rate.domain.Rate</class>
|
||||
<class>org.apache.fineract.portfolio.fund.domain.Fund</class>
|
||||
<class>org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket</class>
|
||||
<class>org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange</class>
|
||||
<class>org.apache.fineract.portfolio.paymenttype.domain.PaymentType</class>
|
||||
<class>org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail</class>
|
||||
<class>org.apache.fineract.portfolio.tax.domain.TaxGroup</class>
|
||||
<class>org.apache.fineract.portfolio.tax.domain.TaxGroupMappings</class>
|
||||
<class>org.apache.fineract.portfolio.tax.domain.TaxComponent</class>
|
||||
<class>org.apache.fineract.portfolio.tax.domain.TaxComponentHistory</class>
|
||||
<class>org.apache.fineract.portfolio.floatingrates.domain.FloatingRate</class>
|
||||
<class>org.apache.fineract.portfolio.floatingrates.domain.FloatingRatePeriod</class>
|
||||
<class>org.apache.fineract.portfolio.calendar.domain.Calendar</class>
|
||||
<class>org.apache.fineract.portfolio.calendar.domain.CalendarHistory</class>
|
||||
<class>org.apache.fineract.portfolio.calendar.domain.CalendarInstance</class>
|
||||
<class>org.apache.fineract.useradministration.domain.AppUser</class>
|
||||
<class>org.apache.fineract.useradministration.domain.Role</class>
|
||||
<class>org.apache.fineract.useradministration.domain.Permission</class>
|
||||
<class>org.apache.fineract.useradministration.domain.AppUserClientMapping</class>
|
||||
<!-- Fineract loan module -->
|
||||
<class>org.apache.fineract.portfolio.collateral.domain.LoanCollateral</class>
|
||||
<class>org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement</class>
|
||||
<class>org.apache.fineract.portfolio.collateralmanagement.domain.CollateralManagementDomain</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.Loan</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCharge</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanOfficerAssignmentHistory</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTopupDetails</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheCharge</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProduct</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProductBorrowerCycleVariations</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProductConfigurableAttributes</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProductFloatingRates</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProductGuaranteeDetails</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProductInterestRecalculationDetails</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProductVariableInstallmentConfig</class>
|
||||
<class>org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.DueType</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.AllocationType</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.AllocationTypeListConverter</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProductCreditAllocationRule</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType</class>
|
||||
<class>org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTypeListConverter</class>
|
||||
<class>org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter</class>
|
||||
<!-- Fineract Charge module -->
|
||||
<class>org.apache.fineract.portfolio.charge.domain.Charge</class>
|
||||
<!-- Fineract Accounting module -->
|
||||
<class>org.apache.fineract.accounting.closure.domain.GLClosure</class>
|
||||
<class>org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount</class>
|
||||
<class>org.apache.fineract.accounting.glaccount.domain.GLAccount</class>
|
||||
<class>org.apache.fineract.accounting.journalentry.domain.JournalEntry</class>
|
||||
<class>org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping</class>
|
||||
<class>org.apache.fineract.accounting.rule.domain.AccountingRule</class>
|
||||
<class>org.apache.fineract.accounting.rule.domain.AccountingTagRule</class>
|
||||
<exclude-unlisted-classes>false</exclude-unlisted-classes>
|
||||
<properties>
|
||||
<property name="eclipselink.weaving" value="static" />
|
||||
</properties>
|
||||
</persistence-unit>
|
||||
</persistence>
|
||||
+618
@@ -0,0 +1,618 @@
|
||||
/**
|
||||
* 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 static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REPAYMENT;
|
||||
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;
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.refEq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
|
||||
import org.apache.fineract.infrastructure.core.domain.ActionContext;
|
||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
|
||||
private final LocalDate transactionDate = LocalDate.of(2023, 7, 11);
|
||||
private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1);
|
||||
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
|
||||
private AdvancedPaymentScheduleTransactionProcessor underTest;
|
||||
private LoanReAgingParameterRepository reAgingParameterRepository = Mockito.mock(LoanReAgingParameterRepository.class);
|
||||
|
||||
@BeforeAll
|
||||
public static void init() {
|
||||
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void destruct() {
|
||||
MONEY_HELPER.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
underTest = new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository);
|
||||
|
||||
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
|
||||
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
|
||||
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, transactionDate)));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
ThreadLocalContextUtil.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chargePaymentTransactionTestWithExactAmount() {
|
||||
final BigDecimal chargeAmount = BigDecimal.valueOf(100);
|
||||
LocalDate disbursementDate = LocalDate.of(2023, 1, 1);
|
||||
LocalDate transactionDate = LocalDate.of(2023, 1, 5);
|
||||
LoanTransaction loanTransaction = mock(LoanTransaction.class);
|
||||
MonetaryCurrency currency = MONETARY_CURRENCY;
|
||||
LoanCharge charge = mock(LoanCharge.class);
|
||||
LoanChargePaidBy chargePaidBy = mock(LoanChargePaidBy.class);
|
||||
Money overpaidAmount = Money.zero(currency);
|
||||
Money zero = Money.zero(currency);
|
||||
Loan loan = mock(Loan.class);
|
||||
Money chargeAmountMoney = Money.of(currency, chargeAmount);
|
||||
LoanRepaymentScheduleInstallment installment = spy(
|
||||
new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L),
|
||||
BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
|
||||
|
||||
when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
|
||||
when(chargePaidBy.getLoanCharge()).thenReturn(charge);
|
||||
when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
|
||||
when(loanTransaction.getAmount()).thenReturn(chargeAmount);
|
||||
when(loanTransaction.getAmount(currency)).thenReturn(chargeAmountMoney);
|
||||
when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
|
||||
when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
|
||||
when(loanTransaction.getLoan()).thenReturn(loan);
|
||||
when(loan.getDisbursementDate()).thenReturn(disbursementDate);
|
||||
when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true);
|
||||
when(installment.getInstallmentNumber()).thenReturn(1);
|
||||
when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
|
||||
when(loanTransaction.isPenaltyPayment()).thenReturn(false);
|
||||
|
||||
underTest.processLatestTransaction(loanTransaction,
|
||||
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
|
||||
|
||||
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
|
||||
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
|
||||
assertEquals(zero.getAmount(), loanTransaction.getAmount(currency).minus(chargeAmountMoney).getAmount());
|
||||
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
|
||||
assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(currency).getAmount()));
|
||||
Mockito.verify(loan, times(0)).getPaymentAllocationRules();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chargePaymentTransactionTestWithLessTransactionAmount() {
|
||||
BigDecimal chargeAmount = BigDecimal.valueOf(100.00);
|
||||
LocalDate disbursementDate = LocalDate.of(2023, 1, 1);
|
||||
LocalDate transactionDate = LocalDate.of(2023, 1, 5);
|
||||
LoanTransaction loanTransaction = mock(LoanTransaction.class);
|
||||
MonetaryCurrency currency = MONETARY_CURRENCY;
|
||||
LoanCharge charge = mock(LoanCharge.class);
|
||||
LoanChargePaidBy chargePaidBy = mock(LoanChargePaidBy.class);
|
||||
Money overpaidAmount = Money.zero(currency);
|
||||
Money zero = Money.zero(currency);
|
||||
Loan loan = mock(Loan.class);
|
||||
Money chargeAmountMoney = Money.of(currency, chargeAmount);
|
||||
BigDecimal transactionAmount = BigDecimal.valueOf(20.00);
|
||||
Money transactionAmountMoney = Money.of(currency, transactionAmount);
|
||||
LoanRepaymentScheduleInstallment installment = spy(
|
||||
new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L),
|
||||
BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
|
||||
|
||||
when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
|
||||
when(chargePaidBy.getLoanCharge()).thenReturn(charge);
|
||||
when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
|
||||
when(loanTransaction.getAmount()).thenReturn(transactionAmount);
|
||||
when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney);
|
||||
when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
|
||||
when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
|
||||
when(loanTransaction.getLoan()).thenReturn(loan);
|
||||
when(loan.getDisbursementDate()).thenReturn(disbursementDate);
|
||||
when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true);
|
||||
when(installment.getInstallmentNumber()).thenReturn(1);
|
||||
when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney);
|
||||
when(loanTransaction.isPenaltyPayment()).thenReturn(false);
|
||||
|
||||
underTest.processLatestTransaction(loanTransaction,
|
||||
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
|
||||
|
||||
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney));
|
||||
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney), refEq(zero));
|
||||
assertEquals(zero.getAmount(), loanTransaction.getAmount(currency).minus(transactionAmountMoney).getAmount());
|
||||
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
|
||||
assertEquals(0, BigDecimal.valueOf(80.00).compareTo(installment.getFeeChargesOutstanding(currency).getAmount()));
|
||||
Mockito.verify(loan, times(0)).getPaymentAllocationRules();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void chargePaymentTransactionTestWithMoreTransactionAmount() {
|
||||
BigDecimal chargeAmount = BigDecimal.valueOf(100.00);
|
||||
LocalDate disbursementDate = LocalDate.of(2023, 1, 1);
|
||||
LocalDate transactionDate = disbursementDate.plusMonths(1);
|
||||
LoanTransaction loanTransaction = mock(LoanTransaction.class);
|
||||
MonetaryCurrency currency = MONETARY_CURRENCY;
|
||||
LoanCharge charge = mock(LoanCharge.class);
|
||||
LoanChargePaidBy chargePaidBy = mock(LoanChargePaidBy.class);
|
||||
Money overpaidAmount = Money.zero(currency);
|
||||
Money zero = Money.zero(currency);
|
||||
Loan loan = mock(Loan.class);
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = mock(LoanProductRelatedDetail.class);
|
||||
Money chargeAmountMoney = Money.of(currency, chargeAmount);
|
||||
BigDecimal transactionAmount = BigDecimal.valueOf(120.00);
|
||||
Money transactionAmountMoney = Money.of(currency, transactionAmount);
|
||||
LoanPaymentAllocationRule loanPaymentAllocationRule = mock(LoanPaymentAllocationRule.class);
|
||||
LoanRepaymentScheduleInstallment installment = spy(new LoanRepaymentScheduleInstallment(loan, 1, disbursementDate, transactionDate,
|
||||
BigDecimal.valueOf(100L), BigDecimal.valueOf(0L), chargeAmount, BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
|
||||
|
||||
when(loanTransaction.getTypeOf()).thenReturn(LoanTransactionType.CHARGE_PAYMENT);
|
||||
when(chargePaidBy.getLoanCharge()).thenReturn(charge);
|
||||
when(loanTransaction.getLoanChargesPaid()).thenReturn(Set.of(chargePaidBy));
|
||||
when(loanTransaction.getAmount()).thenReturn(transactionAmount);
|
||||
when(loanTransaction.getAmount(currency)).thenReturn(transactionAmountMoney);
|
||||
when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
|
||||
when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
|
||||
when(loanTransaction.getLoan()).thenReturn(loan);
|
||||
when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
|
||||
when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL);
|
||||
when(loan.getDisbursementDate()).thenReturn(disbursementDate);
|
||||
when(charge.isDueForCollectionFromIncludingAndUpToAndIncluding(disbursementDate, installment.getDueDate())).thenReturn(true);
|
||||
when(installment.getInstallmentNumber()).thenReturn(1);
|
||||
when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
|
||||
when(loanTransaction.isPenaltyPayment()).thenReturn(false);
|
||||
when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule));
|
||||
when(loanPaymentAllocationRule.getTransactionType()).thenReturn(PaymentAllocationTransactionType.DEFAULT);
|
||||
when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL));
|
||||
when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true);
|
||||
|
||||
underTest.processLatestTransaction(loanTransaction,
|
||||
new TransactionCtx(currency, List.of(installment), Set.of(charge), new MoneyHolder(overpaidAmount)));
|
||||
|
||||
Mockito.verify(installment, times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
|
||||
Mockito.verify(loanTransaction, times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
|
||||
assertEquals(0, BigDecimal.valueOf(20).compareTo(loanTransaction.getAmount(currency).minus(chargeAmountMoney).getAmount()));
|
||||
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
|
||||
assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(currency).getAmount()));
|
||||
assertEquals(0, BigDecimal.valueOf(80).compareTo(installment.getPrincipalOutstanding(currency).getAmount()));
|
||||
Mockito.verify(loan, times(1)).getPaymentAllocationRules();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessCreditTransactionWithAllocationRulePrincipalPenaltyFeeInterest() {
|
||||
// given
|
||||
Loan loan = mock(Loan.class);
|
||||
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
|
||||
|
||||
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST);
|
||||
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
||||
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
|
||||
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
|
||||
|
||||
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
||||
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
|
||||
LoanRepaymentScheduleInstallment installment = createMockInstallment(LocalDate.of(2023, 1, 31), false);
|
||||
installments.add(installment);
|
||||
|
||||
// when
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
|
||||
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||
|
||||
// verify principal
|
||||
Mockito.verify(installment, times(1)).addToCreditedPrincipal(new BigDecimal("10.00"));
|
||||
ArgumentCaptor<LocalDate> localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.class);
|
||||
ArgumentCaptor<Money> moneyCaptor = ArgumentCaptor.forClass(Money.class);
|
||||
Mockito.verify(installment, times(1)).addToPrincipal(localDateArgumentCaptor.capture(), moneyCaptor.capture());
|
||||
Assertions.assertEquals(LocalDate.of(2023, 1, 1), localDateArgumentCaptor.getValue());
|
||||
assertEquals(0, moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
|
||||
// verify charges on installment
|
||||
Mockito.verify(installment, times(1)).addToCreditedFee(new BigDecimal("10.00"));
|
||||
Mockito.verify(installment, times(1)).addToCreditedPenalty(new BigDecimal("5.00"));
|
||||
|
||||
ArgumentCaptor<Money> feeCaptor = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> penaltyCaptor = ArgumentCaptor.forClass(Money.class);
|
||||
Mockito.verify(installment, times(2)).addToChargePortion(feeCaptor.capture(), any(), any(), penaltyCaptor.capture(), any(), any());
|
||||
assertEquals(2, feeCaptor.getAllValues().size());
|
||||
assertEquals(2, penaltyCaptor.getAllValues().size());
|
||||
|
||||
assertEquals(0, feeCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
assertEquals(0, feeCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.ZERO));
|
||||
|
||||
assertEquals(0, penaltyCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.ZERO));
|
||||
assertEquals(0, penaltyCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.valueOf(5.0)));
|
||||
|
||||
// verify transaction
|
||||
ArgumentCaptor<Money> principal = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
|
||||
Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
|
||||
penalty.capture());
|
||||
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0)));
|
||||
assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.ZERO));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessCreditTransactionWithAllocationRulePenaltyFeePrincipalInterest() {
|
||||
// given
|
||||
Loan loan = mock(Loan.class);
|
||||
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
|
||||
|
||||
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PENALTY, FEE, PRINCIPAL, INTEREST);
|
||||
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
||||
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
|
||||
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
|
||||
|
||||
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
||||
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
|
||||
LoanRepaymentScheduleInstallment installment = createMockInstallment(LocalDate.of(2023, 1, 31), false);
|
||||
installments.add(installment);
|
||||
|
||||
// when
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
|
||||
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||
|
||||
// verify charges on installment
|
||||
Mockito.verify(installment, times(1)).addToCreditedFee(new BigDecimal("20.00"));
|
||||
Mockito.verify(installment, times(1)).addToCreditedPenalty(new BigDecimal("5.00"));
|
||||
|
||||
ArgumentCaptor<Money> feeCaptor = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> penaltyCaptor = ArgumentCaptor.forClass(Money.class);
|
||||
Mockito.verify(installment, times(2)).addToChargePortion(feeCaptor.capture(), any(), any(), penaltyCaptor.capture(), any(), any());
|
||||
assertEquals(2, feeCaptor.getAllValues().size());
|
||||
assertEquals(2, penaltyCaptor.getAllValues().size());
|
||||
|
||||
assertEquals(0, feeCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.valueOf(20.0)));
|
||||
assertEquals(0, feeCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.ZERO));
|
||||
|
||||
assertEquals(0, penaltyCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.ZERO));
|
||||
assertEquals(0, penaltyCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.valueOf(5.0)));
|
||||
|
||||
// verify transaction
|
||||
ArgumentCaptor<Money> principal = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
|
||||
Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
|
||||
penalty.capture());
|
||||
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(0)));
|
||||
assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.valueOf(0)));
|
||||
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(20.0)));
|
||||
assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWithAdditionalInstallment() {
|
||||
// given
|
||||
Loan loan = mock(Loan.class);
|
||||
LoanTransaction chargeBackTransaction = createChargebackTransaction(loan, 25.0);
|
||||
|
||||
LoanCreditAllocationRule mockCreditAllocationRule = createMockCreditAllocationRule(PRINCIPAL, PENALTY, FEE, INTEREST);
|
||||
when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
|
||||
LoanTransaction repayment = createRepayment(loan, chargeBackTransaction, 10, 0, 20, 5);
|
||||
lenient().when(loan.getLoanTransactions()).thenReturn(List.of(repayment));
|
||||
|
||||
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(MONETARY_CURRENCY));
|
||||
List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>();
|
||||
|
||||
LoanRepaymentScheduleInstallment installment1 = createMockInstallment(LocalDate.of(2022, 12, 20), false);
|
||||
LoanRepaymentScheduleInstallment installment2 = createMockInstallment(LocalDate.of(2022, 12, 27), true);
|
||||
installments.add(installment1);
|
||||
installments.add(installment2);
|
||||
|
||||
// when
|
||||
TransactionCtx ctx = new TransactionCtx(MONETARY_CURRENCY, installments, null, overpaymentHolder);
|
||||
underTest.processCreditTransaction(chargeBackTransaction, ctx);
|
||||
|
||||
// verify principal
|
||||
Mockito.verify(installment2, times(1)).addToCreditedPrincipal(new BigDecimal("10.00"));
|
||||
ArgumentCaptor<LocalDate> localDateArgumentCaptor = ArgumentCaptor.forClass(LocalDate.class);
|
||||
ArgumentCaptor<Money> moneyCaptor = ArgumentCaptor.forClass(Money.class);
|
||||
Mockito.verify(installment2, times(1)).addToPrincipal(localDateArgumentCaptor.capture(), moneyCaptor.capture());
|
||||
Assertions.assertEquals(LocalDate.of(2023, 1, 1), localDateArgumentCaptor.getValue());
|
||||
assertEquals(0, moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
|
||||
// verify charges on installment
|
||||
Mockito.verify(installment2, times(1)).addToCreditedFee(new BigDecimal("10.00"));
|
||||
Mockito.verify(installment2, times(1)).addToCreditedPenalty(new BigDecimal("5.00"));
|
||||
|
||||
ArgumentCaptor<Money> feeCaptor = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> penaltyCaptor = ArgumentCaptor.forClass(Money.class);
|
||||
Mockito.verify(installment2, times(2)).addToChargePortion(feeCaptor.capture(), any(), any(), penaltyCaptor.capture(), any(), any());
|
||||
assertEquals(2, feeCaptor.getAllValues().size());
|
||||
assertEquals(2, penaltyCaptor.getAllValues().size());
|
||||
|
||||
assertEquals(0, feeCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
assertEquals(0, feeCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.ZERO));
|
||||
|
||||
assertEquals(0, penaltyCaptor.getAllValues().get(0).getAmount().compareTo(BigDecimal.ZERO));
|
||||
assertEquals(0, penaltyCaptor.getAllValues().get(1).getAmount().compareTo(BigDecimal.valueOf(5.0)));
|
||||
|
||||
// verify transaction
|
||||
ArgumentCaptor<Money> principal = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
|
||||
ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
|
||||
Mockito.verify(chargeBackTransaction, times(1)).updateComponents(principal.capture(), interest.capture(), fee.capture(),
|
||||
penalty.capture());
|
||||
assertEquals(0, principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
|
||||
assertEquals(0, penalty.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0)));
|
||||
assertEquals(0, interest.getValue().getAmount().compareTo(BigDecimal.ZERO));
|
||||
}
|
||||
|
||||
private LoanRepaymentScheduleInstallment createMockInstallment(LocalDate localDate, boolean isAdditional) {
|
||||
LoanRepaymentScheduleInstallment installment = mock(LoanRepaymentScheduleInstallment.class);
|
||||
lenient().when(installment.isAdditional()).thenReturn(isAdditional);
|
||||
lenient().when(installment.getDueDate()).thenReturn(localDate);
|
||||
Money interestCharged = Money.of(MONETARY_CURRENCY, BigDecimal.ZERO);
|
||||
lenient().when(installment.getInterestCharged(MONETARY_CURRENCY)).thenReturn(interestCharged);
|
||||
return installment;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private LoanCreditAllocationRule createMockCreditAllocationRule(AllocationType... allocationTypes) {
|
||||
LoanCreditAllocationRule mockCreditAllocationRule = mock(LoanCreditAllocationRule.class);
|
||||
lenient().when(mockCreditAllocationRule.getTransactionType()).thenReturn(CreditAllocationTransactionType.CHARGEBACK);
|
||||
lenient().when(mockCreditAllocationRule.getAllocationTypes()).thenReturn(Arrays.asList(allocationTypes));
|
||||
return mockCreditAllocationRule;
|
||||
}
|
||||
|
||||
private LoanTransaction createRepayment(Loan loan, LoanTransaction toTransaction, double principalPortion, double interestPortion,
|
||||
double feePortion, double penaltyPortion) {
|
||||
LoanTransaction repayment = mock(LoanTransaction.class);
|
||||
lenient().when(repayment.getLoan()).thenReturn(loan);
|
||||
lenient().when(repayment.isRepayment()).thenReturn(true);
|
||||
lenient().when(repayment.getTypeOf()).thenReturn(REPAYMENT);
|
||||
lenient().when(repayment.getPrincipalPortion()).thenReturn(BigDecimal.valueOf(principalPortion));
|
||||
lenient().when(repayment.getInterestPortion()).thenReturn(BigDecimal.valueOf(interestPortion));
|
||||
lenient().when(repayment.getFeeChargesPortion()).thenReturn(BigDecimal.valueOf(feePortion));
|
||||
lenient().when(repayment.getPenaltyChargesPortion()).thenReturn(BigDecimal.valueOf(penaltyPortion));
|
||||
|
||||
LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
|
||||
lenient().when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
|
||||
lenient().when(relation.getToTransaction()).thenReturn(toTransaction);
|
||||
|
||||
lenient().when(repayment.getLoanTransactionRelations()).thenReturn(Set.of(relation));
|
||||
return repayment;
|
||||
}
|
||||
|
||||
private LoanTransaction createChargebackTransaction(Loan loan, double transactionAmount) {
|
||||
LoanTransaction chargeback = mock(LoanTransaction.class);
|
||||
lenient().when(chargeback.isChargeback()).thenReturn(true);
|
||||
lenient().when(chargeback.getTypeOf()).thenReturn(LoanTransactionType.CHARGEBACK);
|
||||
lenient().when(chargeback.getLoan()).thenReturn(loan);
|
||||
lenient().when(chargeback.getAmount()).thenReturn(BigDecimal.valueOf(transactionAmount));
|
||||
Money money = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(transactionAmount));
|
||||
lenient().when(chargeback.getAmount(MONETARY_CURRENCY)).thenReturn(money);
|
||||
lenient().when(chargeback.getTransactionDate()).thenReturn(LocalDate.of(2023, 1, 1));
|
||||
return chargeback;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateChargebackAllocationMap() {
|
||||
Map<AllocationType, Money> result;
|
||||
MonetaryCurrency currency = mock(MonetaryCurrency.class);
|
||||
|
||||
result = underTest.calculateChargebackAllocationMap(allocationMap(50.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(50.0),
|
||||
List.of(PRINCIPAL, INTEREST, FEE, PENALTY), currency);
|
||||
verify(allocationMap(50.0, 0, 0, 0, currency), result);
|
||||
|
||||
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(50.0),
|
||||
List.of(PRINCIPAL, INTEREST, FEE, PENALTY), currency);
|
||||
verify(allocationMap(40.0, 10, 0, 0, currency), result);
|
||||
|
||||
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(50.0),
|
||||
List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency);
|
||||
verify(allocationMap(40.0, 0, 10, 0, currency), result);
|
||||
|
||||
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(340.0),
|
||||
List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency);
|
||||
verify(allocationMap(40.0, 88.0, 200.0, 12.0, currency), result);
|
||||
|
||||
result = underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0, 12.0, currency), BigDecimal.valueOf(352.0),
|
||||
List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency);
|
||||
verify(allocationMap(40.0, 100.0, 200.0, 12.0, currency), result);
|
||||
}
|
||||
|
||||
private void verify(Map<AllocationType, Money> expected, Map<AllocationType, Money> actual) {
|
||||
Assertions.assertEquals(expected.size(), actual.size());
|
||||
expected.forEach((k, v) -> {
|
||||
Assertions.assertEquals(0, v.getAmount().compareTo(actual.get(k).getAmount()), "Not matching for " + k);
|
||||
});
|
||||
}
|
||||
|
||||
private Map<AllocationType, Money> allocationMap(double principal, double interest, double fee, double penalty,
|
||||
MonetaryCurrency currency) {
|
||||
Map<AllocationType, Money> allocationMap = new HashMap<>();
|
||||
allocationMap.put(AllocationType.PRINCIPAL, Money.of(currency, BigDecimal.valueOf(principal)));
|
||||
allocationMap.put(AllocationType.INTEREST, Money.of(currency, BigDecimal.valueOf(interest)));
|
||||
allocationMap.put(AllocationType.FEE, Money.of(currency, BigDecimal.valueOf(fee)));
|
||||
allocationMap.put(AllocationType.PENALTY, Money.of(currency, BigDecimal.valueOf(penalty)));
|
||||
return allocationMap;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionWhenIdProvided() {
|
||||
// given
|
||||
LoanTransaction chargebackTransaction = mock(LoanTransaction.class);
|
||||
when(chargebackTransaction.getId()).thenReturn(123L);
|
||||
Loan loan = mock(Loan.class);
|
||||
when(chargebackTransaction.getLoan()).thenReturn(loan);
|
||||
LoanTransaction repayment1 = mock(LoanTransaction.class);
|
||||
LoanTransaction repayment2 = mock(LoanTransaction.class);
|
||||
when(loan.getLoanTransactions()).thenReturn(List.of(chargebackTransaction, repayment1, repayment2));
|
||||
|
||||
LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
|
||||
when(relation.getToTransaction()).thenReturn(chargebackTransaction);
|
||||
when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
|
||||
when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
|
||||
TransactionCtx ctx = mock(TransactionCtx.class);
|
||||
|
||||
// when
|
||||
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackTransaction, ctx);
|
||||
|
||||
// then
|
||||
Assertions.assertEquals(originalTransaction, repayment2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindOriginalTransactionThrowsRuntimeExceptionWhenIdProvidedAndRelationsAreMissing() {
|
||||
// given
|
||||
LoanTransaction chargebackTransaction = mock(LoanTransaction.class);
|
||||
when(chargebackTransaction.getId()).thenReturn(123L);
|
||||
Loan loan = mock(Loan.class);
|
||||
when(chargebackTransaction.getLoan()).thenReturn(loan);
|
||||
LoanTransaction repayment1 = mock(LoanTransaction.class);
|
||||
LoanTransaction repayment2 = mock(LoanTransaction.class);
|
||||
when(loan.getLoanTransactions()).thenReturn(List.of(chargebackTransaction, repayment1, repayment2));
|
||||
|
||||
when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of());
|
||||
|
||||
TransactionCtx ctx = mock(TransactionCtx.class);
|
||||
|
||||
// when + then
|
||||
RuntimeException runtimeException = Assertions.assertThrows(RuntimeException.class,
|
||||
() -> underTest.findOriginalTransaction(chargebackTransaction, ctx));
|
||||
Assertions.assertEquals("Chargeback transaction must have an original transaction", runtimeException.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvided() {
|
||||
// given
|
||||
LoanTransaction chargebackReplayed = mock(LoanTransaction.class);
|
||||
when(chargebackReplayed.getId()).thenReturn(null);
|
||||
LoanTransaction repayment1 = mock(LoanTransaction.class);
|
||||
LoanTransaction repayment2 = mock(LoanTransaction.class);
|
||||
|
||||
LoanTransaction originalChargeback = mock(LoanTransaction.class);
|
||||
when(originalChargeback.getId()).thenReturn(123L);
|
||||
LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
|
||||
when(relation.getToTransaction()).thenReturn(originalChargeback);
|
||||
when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
|
||||
when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
|
||||
|
||||
TransactionCtx ctx = mock(TransactionCtx.class);
|
||||
ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class);
|
||||
when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail);
|
||||
when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L));
|
||||
when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of(122L, repayment1, 121L, repayment2));
|
||||
|
||||
// when
|
||||
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
|
||||
|
||||
// then
|
||||
Assertions.assertEquals(originalTransaction, repayment2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFindOriginalTransactionShouldFindOriginalInLoansTransactionFromTransactionCtxWhenIdIsNotProvidedFallbackToPersistedTransactions() {
|
||||
// given
|
||||
LoanTransaction chargebackReplayed = mock(LoanTransaction.class);
|
||||
when(chargebackReplayed.getId()).thenReturn(null);
|
||||
LoanTransaction repayment1 = mock(LoanTransaction.class);
|
||||
LoanTransaction repayment2 = mock(LoanTransaction.class);
|
||||
Loan loan = mock(Loan.class);
|
||||
when(chargebackReplayed.getLoan()).thenReturn(loan);
|
||||
when(loan.getLoanTransactions()).thenReturn(List.of(repayment1, repayment2));
|
||||
|
||||
LoanTransaction originalChargeback = mock(LoanTransaction.class);
|
||||
when(originalChargeback.getId()).thenReturn(123L);
|
||||
LoanTransactionRelation relation = mock(LoanTransactionRelation.class);
|
||||
when(relation.getToTransaction()).thenReturn(originalChargeback);
|
||||
when(relation.getRelationType()).thenReturn(LoanTransactionRelationTypeEnum.CHARGEBACK);
|
||||
when(repayment2.getLoanTransactionRelations()).thenReturn(Set.of(relation));
|
||||
|
||||
TransactionCtx ctx = mock(TransactionCtx.class);
|
||||
ChangedTransactionDetail changedTransactionDetail = mock(ChangedTransactionDetail.class);
|
||||
when(ctx.getChangedTransactionDetail()).thenReturn(changedTransactionDetail);
|
||||
when(changedTransactionDetail.getCurrentTransactionToOldId()).thenReturn(Map.of(chargebackReplayed, 123L));
|
||||
when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of());
|
||||
|
||||
// when
|
||||
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
|
||||
|
||||
// then
|
||||
Assertions.assertEquals(originalTransaction, repayment2);
|
||||
}
|
||||
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 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 com.google.common.collect.Collections2;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class ChargeOrTransactionTest {
|
||||
|
||||
@Test
|
||||
public void testCompareToEqual() {
|
||||
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
Assertions.assertTrue(charge.compareTo(transaction) == 0);
|
||||
Assertions.assertTrue(transaction.compareTo(charge) == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompareToEqualBackdatedCharge() {
|
||||
ChargeOrTransaction charge = createCharge("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
ChargeOrTransaction transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
Assertions.assertTrue(charge.compareTo(transaction) == 0);
|
||||
Assertions.assertTrue(transaction.compareTo(charge) == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompareToCreatedDateTime() {
|
||||
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00");
|
||||
ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
Assertions.assertTrue(charge.compareTo(transaction) > 0);
|
||||
Assertions.assertTrue(transaction.compareTo(charge) < 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompareToSubmittedOnDate() {
|
||||
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-16", "2023-10-17T10:15:30+01:00");
|
||||
Assertions.assertTrue(charge.compareTo(transaction) > 0);
|
||||
Assertions.assertTrue(transaction.compareTo(charge) < 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComparatorEffectiveDate() {
|
||||
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
ChargeOrTransaction transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
Assertions.assertTrue(charge.compareTo(transaction) > 0);
|
||||
Assertions.assertTrue(transaction.compareTo(charge) < 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComparatorOnDifferentSubmittedDay() {
|
||||
ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:16:30+01:00");
|
||||
ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-18", "2023-10-18T10:14:30+01:00");
|
||||
Collection<List<ChargeOrTransaction>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
|
||||
List<ChargeOrTransaction> expected = List.of(cot1, cot3, cot2);
|
||||
for (List<ChargeOrTransaction> permutation : permutations) {
|
||||
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComparatorOnSameDayBackdatedCharge() {
|
||||
ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:31+01:00");
|
||||
ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:15:33+01:00");
|
||||
ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:32+01:00");
|
||||
Collection<List<ChargeOrTransaction>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
|
||||
List<ChargeOrTransaction> expected = List.of(cot1, cot3, cot2);
|
||||
for (List<ChargeOrTransaction> permutation : permutations) {
|
||||
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComparatorOnSameDay() {
|
||||
ChargeOrTransaction cot1 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:31+01:00");
|
||||
ChargeOrTransaction cot2 = createTransaction("2023-10-19", "2023-10-19", "2023-10-19T10:15:33+01:00");
|
||||
ChargeOrTransaction cot3 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:32+01:00");
|
||||
Collection<List<ChargeOrTransaction>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
|
||||
List<ChargeOrTransaction> expected = List.of(cot1, cot3, cot2);
|
||||
for (List<ChargeOrTransaction> permutation : permutations) {
|
||||
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
|
||||
}
|
||||
}
|
||||
|
||||
private ChargeOrTransaction createCharge(String effectiveDate, String submittedDate, String creationDateTime) {
|
||||
LoanCharge charge = Mockito.mock(LoanCharge.class);
|
||||
Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate));
|
||||
Mockito.when(charge.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate));
|
||||
Mockito.when(charge.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.parse(creationDateTime)));
|
||||
return new ChargeOrTransaction(charge);
|
||||
}
|
||||
|
||||
private ChargeOrTransaction createTransaction(String transactionDate, String submittedDate, String creationDateTime) {
|
||||
LoanTransaction transaction = Mockito.mock(LoanTransaction.class);
|
||||
Mockito.when(transaction.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate));
|
||||
Mockito.when(transaction.getTransactionDate()).thenReturn(LocalDate.parse(transactionDate));
|
||||
Mockito.when(transaction.getCreatedDateTime()).thenReturn(OffsetDateTime.parse(creationDateTime));
|
||||
return new ChargeOrTransaction(transaction);
|
||||
}
|
||||
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
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;
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductCreditAllocationRule;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class LoanProductCreditAllocationRuleMergerTest {
|
||||
|
||||
private LoanProductCreditAllocationRuleMerger underTest = new LoanProductCreditAllocationRuleMerger();
|
||||
|
||||
@Test
|
||||
public void testMergerOneNewAdded() {
|
||||
LoanProduct loanProduct = new LoanProduct();
|
||||
|
||||
LoanProductCreditAllocationRule rule1 = createRule(CreditAllocationTransactionType.CHARGEBACK,
|
||||
List.of(PENALTY, FEE, INTEREST, PRINCIPAL));
|
||||
|
||||
boolean result = underTest.updateCreditAllocationRules(loanProduct, List.of(rule1));
|
||||
Assertions.assertTrue(result);
|
||||
Assertions.assertEquals(1, loanProduct.getCreditAllocationRules().size());
|
||||
Assertions.assertEquals(rule1, loanProduct.getCreditAllocationRules().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergerExistingUpdated() {
|
||||
LoanProduct loanProduct = new LoanProduct();
|
||||
|
||||
LoanProductCreditAllocationRule rule1 = createRule(CreditAllocationTransactionType.CHARGEBACK,
|
||||
List.of(PENALTY, FEE, INTEREST, PRINCIPAL));
|
||||
LoanProductCreditAllocationRule rule2 = createRule(CreditAllocationTransactionType.CHARGEBACK,
|
||||
List.of(FEE, INTEREST, PRINCIPAL, PENALTY));
|
||||
|
||||
loanProduct.getCreditAllocationRules().add(rule1);
|
||||
|
||||
boolean result = underTest.updateCreditAllocationRules(loanProduct, List.of(rule2));
|
||||
Assertions.assertTrue(result);
|
||||
Assertions.assertEquals(1, loanProduct.getCreditAllocationRules().size());
|
||||
Assertions.assertEquals(rule2.getTransactionType(), loanProduct.getCreditAllocationRules().get(0).getTransactionType());
|
||||
Assertions.assertEquals(rule2.getAllocationTypes(), loanProduct.getCreditAllocationRules().get(0).getAllocationTypes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergerNothingIsChanged() {
|
||||
LoanProduct loanProduct = new LoanProduct();
|
||||
|
||||
LoanProductCreditAllocationRule rule1 = createRule(CreditAllocationTransactionType.CHARGEBACK,
|
||||
List.of(PENALTY, FEE, INTEREST, PRINCIPAL));
|
||||
LoanProductCreditAllocationRule rule2 = createRule(CreditAllocationTransactionType.CHARGEBACK,
|
||||
List.of(PENALTY, FEE, INTEREST, PRINCIPAL));
|
||||
|
||||
loanProduct.getCreditAllocationRules().add(rule1);
|
||||
|
||||
boolean result = underTest.updateCreditAllocationRules(loanProduct, List.of(rule2));
|
||||
Assertions.assertFalse(result);
|
||||
Assertions.assertEquals(1, loanProduct.getCreditAllocationRules().size());
|
||||
Assertions.assertEquals(rule1.getTransactionType(), loanProduct.getCreditAllocationRules().get(0).getTransactionType());
|
||||
Assertions.assertEquals(rule1.getAllocationTypes(), loanProduct.getCreditAllocationRules().get(0).getAllocationTypes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergerExistingDeleted() {
|
||||
LoanProduct loanProduct = new LoanProduct();
|
||||
|
||||
LoanProductCreditAllocationRule rule1 = createRule(CreditAllocationTransactionType.CHARGEBACK,
|
||||
List.of(PENALTY, FEE, INTEREST, PRINCIPAL));
|
||||
|
||||
loanProduct.getCreditAllocationRules().add(rule1);
|
||||
|
||||
boolean result = underTest.updateCreditAllocationRules(loanProduct, List.of());
|
||||
Assertions.assertTrue(result);
|
||||
Assertions.assertEquals(0, loanProduct.getCreditAllocationRules().size());
|
||||
}
|
||||
|
||||
public LoanProductCreditAllocationRule createRule(CreditAllocationTransactionType transactionType,
|
||||
List<AllocationType> allocationTypeList) {
|
||||
return new LoanProductCreditAllocationRule(null, transactionType, allocationTypeList);
|
||||
}
|
||||
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
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.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType.DUE_INTEREST;
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType.PAST_DUE_FEE;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class LoanProductPaymentAllocationRuleMergerTest {
|
||||
|
||||
@Test
|
||||
public void testMergerOneNewAdded() {
|
||||
// given
|
||||
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
|
||||
LoanProduct loanProduct = new LoanProduct();
|
||||
|
||||
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
|
||||
|
||||
// when
|
||||
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule1));
|
||||
|
||||
// then
|
||||
Assertions.assertTrue(result);
|
||||
Assertions.assertEquals(1, loanProduct.getPaymentAllocationRules().size());
|
||||
Assertions.assertEquals(rule1, loanProduct.getPaymentAllocationRules().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeExistingUpdated() {
|
||||
// given
|
||||
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
|
||||
LoanProduct loanProduct = new LoanProduct();
|
||||
|
||||
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
|
||||
LoanProductPaymentAllocationRule rule2 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(PAST_DUE_FEE));
|
||||
|
||||
loanProduct.getPaymentAllocationRules().add(rule1);
|
||||
|
||||
// when
|
||||
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule2));
|
||||
|
||||
// then
|
||||
Assertions.assertTrue(result);
|
||||
Assertions.assertEquals(1, loanProduct.getPaymentAllocationRules().size());
|
||||
Assertions.assertEquals(PAST_DUE_FEE, loanProduct.getPaymentAllocationRules().get(0).getAllocationTypes().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNothingChanged() {
|
||||
// given
|
||||
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
|
||||
LoanProduct loanProduct = new LoanProduct();
|
||||
|
||||
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
|
||||
LoanProductPaymentAllocationRule rule2 = createRule(REPAYMENT, LAST_INSTALLMENT, List.of(PAST_DUE_FEE));
|
||||
|
||||
loanProduct.getPaymentAllocationRules().addAll(List.of(rule1, rule2));
|
||||
|
||||
// when
|
||||
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule2, rule1));
|
||||
|
||||
// then
|
||||
Assertions.assertFalse(result);
|
||||
Assertions.assertEquals(2, loanProduct.getPaymentAllocationRules().size());
|
||||
Assertions.assertEquals(rule1, loanProduct.getPaymentAllocationRules().get(0));
|
||||
Assertions.assertEquals(rule2, loanProduct.getPaymentAllocationRules().get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergerExistingDeleted() {
|
||||
// given
|
||||
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
|
||||
LoanProduct loanProduct = new LoanProduct();
|
||||
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
|
||||
loanProduct.getPaymentAllocationRules().add(rule1);
|
||||
|
||||
// when
|
||||
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of());
|
||||
|
||||
// then
|
||||
Assertions.assertTrue(result);
|
||||
Assertions.assertEquals(0, loanProduct.getPaymentAllocationRules().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeOneOriginalOneAdded() {
|
||||
// given
|
||||
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
|
||||
LoanProduct loanProduct = new LoanProduct();
|
||||
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
|
||||
loanProduct.getPaymentAllocationRules().add(rule1);
|
||||
LoanProductPaymentAllocationRule rule2 = createRule(REPAYMENT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
|
||||
|
||||
// when
|
||||
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule1, rule2));
|
||||
|
||||
// then
|
||||
Assertions.assertTrue(result);
|
||||
Assertions.assertEquals(2, loanProduct.getPaymentAllocationRules().size());
|
||||
Assertions.assertEquals(rule1, loanProduct.getPaymentAllocationRules().get(0));
|
||||
Assertions.assertEquals(rule2, loanProduct.getPaymentAllocationRules().get(1));
|
||||
}
|
||||
|
||||
public LoanProductPaymentAllocationRule createRule(PaymentAllocationTransactionType transactionType,
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule, List<PaymentAllocationType> allocationTypeList) {
|
||||
return new LoanProductPaymentAllocationRule(null, transactionType, allocationTypeList, futureInstallmentAllocationRule);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user