FINERACT-1932: Fineract modularization Fineract progressive loan module

This commit is contained in:
Ruchi Dhamankar
2024-05-24 14:35:34 +02:00
committed by Adam Saghy
parent f59ae64222
commit 53f4864980
53 changed files with 364 additions and 2 deletions
@@ -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;
}
}
@@ -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);
}
}
@@ -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()));
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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)));
}
}
@@ -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;
}
}
@@ -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)));
}
}
@@ -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();
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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>
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
}