blob: 0b8274dbaf9565cf8962891e0f707b1328191b95 [file] [log] [blame]
/*
* 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.cn.individuallending.internal.service.schedule;
import org.apache.fineract.cn.individuallending.api.v1.domain.caseinstance.CaseParameters;
import org.apache.fineract.cn.individuallending.api.v1.domain.workflow.Action;
import org.apache.fineract.cn.portfolio.api.v1.domain.PaymentCycle;
import org.apache.fineract.cn.portfolio.api.v1.domain.TermRange;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
import static org.apache.fineract.cn.individuallending.internal.service.Fixture.*;
/**
* @author Myrle Krantz
*/
@RunWith(Parameterized.class)
public class ScheduledActionHelpersTest {
private static class TestCase
{
final String description;
LocalDate initialDisbursementDate;
CaseParameters caseParameters;
List<ScheduledAction> expectedResultContents = Collections.emptyList();
long expectedPaymentCount;
long expectedInterestCount;
private LocalDate earliestActionDate;
private LocalDate latestActionDate;
TestCase(final String description) {
this.description = description;
}
TestCase initialDisbursementDate(final LocalDate newVal) {
this.initialDisbursementDate = newVal;
return this;
}
TestCase caseParameters(final CaseParameters newVal) {
this.caseParameters = newVal;
return this;
}
TestCase expectedResultContents(final List<ScheduledAction> newVal) {
this.expectedResultContents = newVal;
return this;
}
TestCase expectedDateRangeOfSchedule(final LocalDate from, final LocalDate to) {
this.earliestActionDate = from;
this.latestActionDate = to;
return this;
}
TestCase expectedPaymentCount(final long newVal) {
this.expectedPaymentCount = newVal;
return this;
}
TestCase expectedInterestCount(final long newVal) {
this.expectedInterestCount = newVal;
return this;
}
@Override
public String toString() {
return "TestCase{" +
"description='" + description + '\'' +
'}';
}
}
@Parameterized.Parameters
public static Collection testCases() {
final Collection<TestCase> ret = new ArrayList<>();
ret.add(simpleTestCase());
ret.add(monthlyPaymentNotAlignedCase());
ret.add(monthlyPaymentAlignedByDayCase());
ret.add(biWeeklyPaymentAlignedByDayOfWeekCase());
ret.add(endOfMonthLeapYearPaymentCase());
ret.add(firstMondayQuarterlyPaymentCase());
ret.add(lastMondayQuarterlyPaymentCase());
ret.add(lastWeekNoDayCase());
ret.add(yearlyPaymentCase());
ret.add(yearlyPaymentFirstDayOfSecondMonthCase());
ret.add(yearlyPaymentThirdMonthCase());
return ret;
}
private static TestCase simpleTestCase()
{
final LocalDate initialDisbursementDate = LocalDate.of(2017, 1, 20);
final Period firstRepaymentPeriod = new Period(initialDisbursementDate, 1);
return new TestCase("simple").caseParameters(getTestCaseParameters()).initialDisbursementDate(initialDisbursementDate)
.expectedResultContents(Collections.singletonList(scheduledInterestAction(initialDisbursementDate, 1, firstRepaymentPeriod)))
.expectedInterestCount(2)
.expectedPaymentCount(2)
.expectedDateRangeOfSchedule(initialDisbursementDate, initialDisbursementDate.plusDays(2));
}
private static TestCase monthlyPaymentNotAlignedCase()
{
final LocalDate initialDisbursementDate = LocalDate.of(2017, 1, 20);
final CaseParameters caseParameters = getTestCaseParameters();
caseParameters.setTermRange(new TermRange(ChronoUnit.YEARS, 1));
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 1, null, null, null));
return new TestCase("monthlyPaymentNotAligned")
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.expectedInterestCount(365)
.expectedPaymentCount(12)
.expectedDateRangeOfSchedule(initialDisbursementDate, initialDisbursementDate.plusDays(365));
}
private static TestCase monthlyPaymentAlignedByDayCase()
{
final LocalDate initialDisbursementDate = LocalDate.of(2017, 1, 20);
final CaseParameters caseParameters = getTestCaseParameters();
caseParameters.setTermRange(new TermRange(ChronoUnit.YEARS, 1));
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 1, 0, null, null));
return new TestCase("monthlyPaymentAlignedByDay")
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.expectedResultContents(scheduledRepaymentActions(initialDisbursementDate,
LocalDate.of(2017, 2, 1),
LocalDate.of(2017, 3, 1),
LocalDate.of(2017, 4, 1),
LocalDate.of(2017, 5, 1),
LocalDate.of(2017, 6, 1),
LocalDate.of(2017, 7, 1),
LocalDate.of(2017, 8, 1),
LocalDate.of(2017, 9, 1),
LocalDate.of(2017, 10, 1),
LocalDate.of(2017, 11, 1),
LocalDate.of(2017, 12, 1),
LocalDate.of(2018, 1, 1),
LocalDate.of(2018, 2, 1)))
.expectedInterestCount(377)
.expectedPaymentCount(13)
.expectedDateRangeOfSchedule(initialDisbursementDate, LocalDate.of(2018, 2, 1));
}
private static TestCase biWeeklyPaymentAlignedByDayOfWeekCase()
{
final LocalDate initialDisbursementDate = LocalDate.of(2017, 1, 20);
final CaseParameters caseParameters = getTestCaseParameters();
caseParameters.setTermRange(new TermRange(ChronoUnit.MONTHS, 3));
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 2, 3, null, null));
return new TestCase("biWeeklyPaymentAlignedByDayOfWeekCase")
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.expectedResultContents(scheduledRepaymentActions(initialDisbursementDate,
LocalDate.of(2017, 2, 2),
LocalDate.of(2017, 2, 16),
LocalDate.of(2017, 3, 2),
LocalDate.of(2017, 3, 16),
LocalDate.of(2017, 3, 30),
LocalDate.of(2017, 4, 13),
LocalDate.of(2017, 4, 27)))
.expectedInterestCount(97)
.expectedPaymentCount(7)
.expectedDateRangeOfSchedule(initialDisbursementDate, LocalDate.of(2018, 4, 27));
}
private static TestCase endOfMonthLeapYearPaymentCase()
{
final LocalDate initialDisbursementDate = LocalDate.of(2019, 12, 31);
final CaseParameters caseParameters = getTestCaseParameters();
caseParameters.setTermRange(new TermRange(ChronoUnit.YEARS, 1));
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 1, 31, null, null));
return new TestCase("endOfMonthLeapYearPaymentCase")
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.expectedResultContents(scheduledRepaymentActions(initialDisbursementDate,
LocalDate.of(2020, 1, 31),
LocalDate.of(2020, 2, 29),
LocalDate.of(2020, 3, 31),
LocalDate.of(2020, 4, 30),
LocalDate.of(2020, 5, 31),
LocalDate.of(2020, 6, 30),
LocalDate.of(2020, 7, 31),
LocalDate.of(2020, 8, 31),
LocalDate.of(2020, 9, 30),
LocalDate.of(2020, 10, 31),
LocalDate.of(2020, 11, 30),
LocalDate.of(2020, 12, 31)))
.expectedInterestCount(366)
.expectedPaymentCount(12)
.expectedDateRangeOfSchedule(initialDisbursementDate, LocalDate.of(2020, 12, 31));
}
private static TestCase firstMondayQuarterlyPaymentCase()
{
final LocalDate initialDisbursementDate = LocalDate.of(2017, 1, 1);
final CaseParameters caseParameters = getTestCaseParameters();
caseParameters.setTermRange(new TermRange(ChronoUnit.YEARS, 2));
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 3, 0, 0, null));
return new TestCase("firstMondayQuarterlyPaymentCase")
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.expectedResultContents(scheduledRepaymentActions(initialDisbursementDate,
LocalDate.of(2017, 4, 3),
LocalDate.of(2017, 7, 3),
LocalDate.of(2017, 10, 2),
LocalDate.of(2018, 1, 1),
LocalDate.of(2018, 4, 2),
LocalDate.of(2018, 7, 2),
LocalDate.of(2018, 10, 1),
LocalDate.of(2019, 1, 7)))
.expectedInterestCount(736)
.expectedPaymentCount(8)
.expectedDateRangeOfSchedule(initialDisbursementDate, LocalDate.of(2019, 4, 1));
}
private static TestCase lastMondayQuarterlyPaymentCase()
{
final LocalDate initialDisbursementDate = LocalDate.of(2017, 1, 1);
final CaseParameters caseParameters = getTestCaseParameters();
caseParameters.setTermRange(new TermRange(ChronoUnit.YEARS, 2));
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 3, 0, -1, null));
return new TestCase("lastMondayQuarterlyPaymentCase")
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.expectedResultContents(scheduledRepaymentActions(initialDisbursementDate,
LocalDate.of(2017, 4, 24),
LocalDate.of(2017, 7, 31),
LocalDate.of(2017, 10, 30),
LocalDate.of(2018, 1, 29),
LocalDate.of(2018, 4, 30),
LocalDate.of(2018, 7, 30),
LocalDate.of(2018, 10, 29),
LocalDate.of(2019, 1, 28)))
.expectedInterestCount(757)
.expectedPaymentCount(8)
.expectedDateRangeOfSchedule(initialDisbursementDate, LocalDate.of(2019, 1, 28));
}
private static TestCase lastWeekNoDayCase()
{
final LocalDate initialDisbursementDate = LocalDate.of(2017, 1, 1);
final CaseParameters caseParameters = getTestCaseParameters();
caseParameters.setTermRange(new TermRange(ChronoUnit.YEARS, 2));
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 3, null, -1, null));
return new TestCase("lastWeekNoDayCase")
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.expectedResultContents(scheduledRepaymentActions(initialDisbursementDate,
LocalDate.of(2017, 4, 30),
LocalDate.of(2017, 7, 31),
LocalDate.of(2017, 10, 31),
LocalDate.of(2018, 1, 31),
LocalDate.of(2018, 4, 30),
LocalDate.of(2018, 7, 31),
LocalDate.of(2018, 10, 31),
LocalDate.of(2019, 1, 31)))
.expectedInterestCount(760)
.expectedPaymentCount(8)
.expectedDateRangeOfSchedule(initialDisbursementDate, LocalDate.of(2019, 1, 31));
}
private static TestCase yearlyPaymentCase()
{
final LocalDate initialDisbursementDate = LocalDate.of(2017, 1, 5);
final CaseParameters caseParameters = getTestCaseParameters();
caseParameters.setTermRange(new TermRange(ChronoUnit.YEARS, 3));
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.YEARS, 1, null, null, null));
return new TestCase("yearlyPaymentCase")
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.expectedResultContents(scheduledRepaymentActions(initialDisbursementDate,
LocalDate.of(2018, 1, 5),
LocalDate.of(2019, 1, 5),
LocalDate.of(2020, 1, 5)))
.expectedInterestCount(1095)
.expectedPaymentCount(3)
.expectedDateRangeOfSchedule(initialDisbursementDate, LocalDate.of(2020, 1, 5));
}
private static TestCase yearlyPaymentFirstDayOfSecondMonthCase()
{
final LocalDate initialDisbursementDate = LocalDate.of(2017, 1, 5);
final CaseParameters caseParameters = getTestCaseParameters();
caseParameters.setTermRange(new TermRange(ChronoUnit.YEARS, 3));
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.YEARS, 1, 0, null, 1));
return new TestCase("yearlyPaymentFirstDayOfSecondMonthCase")
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.expectedResultContents(scheduledRepaymentActions(initialDisbursementDate,
LocalDate.of(2018, 2, 1),
LocalDate.of(2019, 2, 1),
LocalDate.of(2020, 2, 1)))
.expectedInterestCount(1122)
.expectedPaymentCount(3)
.expectedDateRangeOfSchedule(initialDisbursementDate, LocalDate.of(2020, 2, 1));
}
private static TestCase yearlyPaymentThirdMonthCase()
{
final LocalDate initialDisbursementDate = LocalDate.of(2017, 1, 5);
final CaseParameters caseParameters = getTestCaseParameters();
caseParameters.setTermRange(new TermRange(ChronoUnit.YEARS, 5));
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.YEARS, 1, null, null, 2));
return new TestCase("yearlyPaymentThirdMonthCase")
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.expectedResultContents(scheduledRepaymentActions(initialDisbursementDate,
LocalDate.of(2018, 3, 1),
LocalDate.of(2019, 3, 1),
LocalDate.of(2020, 3, 1),
LocalDate.of(2021, 3, 1),
LocalDate.of(2022, 3, 1)))
.expectedInterestCount(1881)
.expectedPaymentCount(5)
.expectedDateRangeOfSchedule(initialDisbursementDate, LocalDate.of(2022, 3, 1));
}
private final TestCase testCase;
public ScheduledActionHelpersTest(final TestCase testCase)
{
this.testCase = testCase;
}
@Test
public void getScheduledActions() throws Exception {
final List<ScheduledAction> result = ScheduledActionHelpers.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
final List<ScheduledAction> missingExpectedResults = testCase.expectedResultContents.stream()
.filter(expectedResult -> !result.contains(expectedResult))
.collect(Collectors.toList());
Assert.assertTrue("Case " + testCase.description + " missing these expected results " + missingExpectedResults,
missingExpectedResults.isEmpty());
result.forEach(x -> {
Assert.assertTrue(x.toString(), testCase.earliestActionDate.isBefore(x.getWhen()) || testCase.earliestActionDate.isEqual(x.getWhen()));
Assert.assertTrue(x.toString(), testCase.latestActionDate.isAfter(x.getWhen()) || testCase.latestActionDate.isEqual(x.getWhen()));
});
Assert.assertEquals(testCase.expectedPaymentCount, countActionsByType(result, Action.ACCEPT_PAYMENT));
Assert.assertEquals(testCase.expectedInterestCount, countActionsByType(result, Action.APPLY_INTEREST));
Assert.assertEquals(1, countActionsByType(result, Action.APPROVE));
Assert.assertEquals(1, countActionsByType(result, Action.CLOSE));
result.stream().filter(scheduledAction -> !ScheduledActionHelpers.actionHasNoActionPeriod(scheduledAction.getAction()))
.forEach(scheduledAction -> {
Assert.assertNotNull("The action period of " + scheduledAction.toString() + " should not be null.",
scheduledAction.getActionPeriod());
Assert.assertNotNull("The repayment period of " + scheduledAction.toString() + " should not be null.",
scheduledAction.getRepaymentPeriod());
});
Assert.assertTrue(noDuplicatesInResult(result));
Assert.assertTrue(maximumOneInterestPerDay(result));
}
@Test
public void getNextScheduledPayment() throws Exception {
final LocalDate roughEndDate = ScheduledActionHelpers.getRoughEndDate(testCase.initialDisbursementDate, testCase.caseParameters);
testCase.expectedResultContents.stream()
.filter(x -> x.getAction() == Action.ACCEPT_PAYMENT)
.forEach(expectedResultContents -> {
final ScheduledAction nextScheduledPayment = ScheduledActionHelpers.getNextScheduledPayment(
testCase.initialDisbursementDate,
expectedResultContents.getWhen().minusDays(1),
roughEndDate,
testCase.caseParameters);
Assert.assertEquals(expectedResultContents, nextScheduledPayment);
});
final ScheduledAction afterAction = ScheduledActionHelpers.getNextScheduledPayment(
testCase.initialDisbursementDate,
roughEndDate.plusDays(1),
roughEndDate,
testCase.caseParameters);
Assert.assertNotNull(afterAction.getActionPeriod());
Assert.assertTrue(afterAction.getActionPeriod().isLastPeriod());
}
private long countActionsByType(final List<ScheduledAction> scheduledActions, final Action actionToCount) {
return scheduledActions.stream().filter(x -> x.getAction() == actionToCount).count();
}
private boolean maximumOneInterestPerDay(final List<ScheduledAction> result) {
final List<LocalDate> interestDays = result.stream()
.filter(x -> x.getAction() == Action.APPLY_INTEREST)
.map(ScheduledAction::getWhen)
.collect(Collectors.toList());
final Set<LocalDate> interestDaysSet = new HashSet<>();
interestDaysSet.addAll(interestDays);
return (interestDays.size() == interestDaysSet.size());
}
private boolean noDuplicatesInResult(final List<ScheduledAction> result) {
final Set<ScheduledAction> duplicatesRemoved = new HashSet<>();
duplicatesRemoved.addAll(result);
return (duplicatesRemoved.size() == result.size());
}
}