Begin implementation of late fee checking and marking.
* implemented command response.
* fixed late fee behavior in planned payments.
* created test for normal payment schedule.
* fixed last payment behavior in test.
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParameters.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParameters.java
index 7be3e81..f69b387 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParameters.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParameters.java
@@ -50,9 +50,6 @@
@Valid
private PaymentCycle paymentCycle;
- @Range(min = 0)
- private BigDecimal paymentSize;
-
public CaseParameters() {
}
@@ -100,14 +97,6 @@
this.paymentCycle = paymentCycle;
}
- public BigDecimal getPaymentSize() {
- return paymentSize;
- }
-
- public void setPaymentSize(BigDecimal paymentSize) {
- this.paymentSize = paymentSize;
- }
-
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -117,13 +106,12 @@
Objects.equals(creditWorthinessSnapshots, that.creditWorthinessSnapshots) &&
Objects.equals(maximumBalance, that.maximumBalance) &&
Objects.equals(termRange, that.termRange) &&
- Objects.equals(paymentCycle, that.paymentCycle) &&
- Objects.equals(paymentSize, that.paymentSize);
+ Objects.equals(paymentCycle, that.paymentCycle);
}
@Override
public int hashCode() {
- return Objects.hash(customerIdentifier, creditWorthinessSnapshots, maximumBalance, termRange, paymentCycle, paymentSize);
+ return Objects.hash(customerIdentifier, creditWorthinessSnapshots, maximumBalance, termRange, paymentCycle);
}
@Override
@@ -134,7 +122,6 @@
", maximumBalance=" + maximumBalance +
", termRange=" + termRange +
", paymentCycle=" + paymentCycle +
- ", paymentSize=" + paymentSize +
'}';
}
}
\ No newline at end of file
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
index 1adcc04..ffe36ac 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
@@ -344,6 +344,22 @@
@RequestParam(value="forpaymentsize", required = false, defaultValue = "") final BigDecimal forPaymentSize);
+
+ @RequestMapping(
+ value = "/products/{productidentifier}/cases/{caseidentifier}/actions/{actionidentifier}/costcomponents",
+ method = RequestMethod.GET,
+ produces = MediaType.ALL_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE
+ )
+ List<CostComponent> getCostComponentsForAction(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("caseidentifier") final String caseIdentifier,
+ @PathVariable("actionidentifier") final String actionIdentifier,
+ @RequestParam(value="touchingaccounts", required = false, defaultValue = "") final Set<String> forAccountDesignators,
+ @RequestParam(value="forpaymentsize", required = false, defaultValue = "") final BigDecimal forPaymentSize,
+ @RequestParam(value="fordatetime", required = false, defaultValue = "") final String forDateTime);
+
+
@RequestMapping(
value = "/products/{productidentifier}/cases/{caseidentifier}/actions/{actionidentifier}/costcomponents",
method = RequestMethod.GET,
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
index cc28b11..8f102a2 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
@@ -15,8 +15,11 @@
*/
package io.mifos.portfolio.api.v1.domain;
+import io.mifos.core.lang.validation.constraints.ValidLocalDateTimeString;
+
import javax.annotation.Nullable;
import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
@@ -34,7 +37,11 @@
private BigDecimal paymentSize;
private String note;
+
+ @ValidLocalDateTimeString
+ @NotNull
private String createdOn;
+
private String createdBy;
public Command() {
diff --git a/api/src/test/java/io/mifos/portfolio/api/v1/domain/CommandTest.java b/api/src/test/java/io/mifos/portfolio/api/v1/domain/CommandTest.java
index 1372188..128f7a3 100644
--- a/api/src/test/java/io/mifos/portfolio/api/v1/domain/CommandTest.java
+++ b/api/src/test/java/io/mifos/portfolio/api/v1/domain/CommandTest.java
@@ -15,11 +15,14 @@
*/
package io.mifos.portfolio.api.v1.domain;
+import io.mifos.core.lang.DateConverter;
import io.mifos.core.test.domain.ValidationTest;
import io.mifos.core.test.domain.ValidationTestCase;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import java.time.Clock;
+import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -39,6 +42,7 @@
protected Command createValidTestSubject() {
final Command ret = new Command();
ret.setOneTimeAccountAssignments(Collections.emptyList());
+ ret.setCreatedOn(DateConverter.toIsoString(LocalDateTime.now(Clock.systemUTC())));
return ret;
}
diff --git a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
index 62de9e5..8ef3fb6 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -54,11 +54,13 @@
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.math.BigDecimal;
+import java.time.Clock;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* @author Myrle Krantz
@@ -195,6 +197,7 @@
productIdentifier,
caseIdentifier,
action,
+ LocalDateTime.now(Clock.systemUTC()),
oneTimeAccountAssignments,
BigDecimal.ZERO,
event,
@@ -205,6 +208,7 @@
void checkStateTransfer(final String productIdentifier,
final String caseIdentifier,
final Action action,
+ final LocalDateTime actionDateTime,
final List<AccountAssignment> oneTimeAccountAssignments,
final BigDecimal paymentSize,
final String event,
@@ -213,6 +217,7 @@
final Command command = new Command();
command.setOneTimeAccountAssignments(oneTimeAccountAssignments);
command.setPaymentSize(paymentSize);
+ command.setCreatedOn(DateConverter.toIsoString(actionDateTime));
portfolioManager.executeCaseCommand(productIdentifier, caseIdentifier, action.name(), command);
Assert.assertTrue(eventRecorder.wait(event, new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(eventDateTime))));
@@ -271,7 +276,9 @@
amount
);
final Set<CostComponent> setOfCostComponents = new HashSet<>(costComponents);
- final Set<CostComponent> setOfExpectedCostComponents = new HashSet<>(Arrays.asList(expectedCostComponents));
+ final Set<CostComponent> setOfExpectedCostComponents = Stream.of(expectedCostComponents)
+ .filter(x -> x.getAmount().compareTo(BigDecimal.ZERO) != 0)
+ .collect(Collectors.toSet());
Assert.assertEquals(setOfExpectedCostComponents, setOfCostComponents);
}
diff --git a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
index 595d2b4..fa38d9b 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -34,6 +34,8 @@
import java.time.Clock;
import java.time.LocalDateTime;
import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import java.util.stream.Stream;
import static org.mockito.Matchers.argThat;
@@ -60,7 +62,7 @@
static final String TELLER_ONE_ACCOUNT_IDENTIFIER = "7352";
static final String LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER = "7810";
static final String CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER = "1103";
- static final String LOANS_PAYABLE_ACCOUNT_IDENTIFIER ="8530";
+ static final String LOANS_PAYABLE_ACCOUNT_IDENTIFIER ="8690";
static final String LATE_FEE_INCOME_ACCOUNT_IDENTIFIER = "1311";
static final String LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER = "7840";
static final String ARREARS_ALLOWANCE_ACCOUNT_IDENTIFIER = "3010";
@@ -79,13 +81,17 @@
this.account.setBalance(balance);
}
- void addAccountEntry(final String message, final double amount) {
+ synchronized void addAccountEntry(final String message, final double amount) {
final AccountEntry accountEntry = new AccountEntry();
accountEntry.setAmount(amount);
accountEntry.setMessage(message);
accountEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now(Clock.systemUTC())));
accountEntries.add(accountEntry);
}
+
+ synchronized List<AccountEntry> copyAccountEntries() {
+ return new ArrayList<>(accountEntries);
+ }
}
private static void makeAccountResponsive(final Account account, final LocalDateTime creationDate, final LedgerManager ledgerManagerMock) {
@@ -94,7 +100,7 @@
accountMap.put(account.getIdentifier(), accountData);
Mockito.doAnswer(new AccountEntriesStreamAnswer(accountData))
.when(ledgerManagerMock)
- .fetchAccountEntriesStream(Mockito.eq(account.getIdentifier()), Matchers.anyString(), Matchers.anyString(), Matchers.eq("ASC"));
+ .fetchAccountEntriesStream(Mockito.eq(account.getIdentifier()), Matchers.anyString(), Matchers.anyString(), AdditionalMatchers.or(Matchers.eq("DESC"), Matchers.eq("ASC")));
}
@@ -425,10 +431,20 @@
@Override
public Stream<AccountEntry> answer(final InvocationOnMock invocation) throws Throwable {
final String message = invocation.getArgumentAt(2, String.class);
- if (message != null)
- return accountData.accountEntries.stream().filter(x -> x.getMessage().equals(message));
- else
- return accountData.accountEntries.stream();
+ final String direction = invocation.getArgumentAt(3, String.class);
+ final boolean asc = direction == null || direction.equals("ASC");
+ final List<AccountEntry> accountEntries = accountData.copyAccountEntries();
+ final int entryCount = accountEntries.size();
+ final Stream<AccountEntry> orderedCorrectly = asc ?
+ IntStream.rangeClosed(1, entryCount).mapToObj(i -> accountEntries.get(entryCount - i)) :
+ accountEntries.stream();
+
+ if (message != null) {
+ return orderedCorrectly.filter(x -> x.getMessage().equals(message));
+ }
+ else {
+ return orderedCorrectly;
+ }
}
}
@@ -494,8 +510,10 @@
final String productIdentifier,
final String caseIdentifier,
final Action action) {
- final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(debtors, creditors, productIdentifier, caseIdentifier, action);
- Mockito.verify(ledgerManager).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
+ final Set<Debtor> filteredDebtors = debtors.stream().filter(x -> BigDecimal.valueOf(Double.valueOf(x.getAmount())).compareTo(BigDecimal.ZERO) != 0).collect(Collectors.toSet());
+ final Set<Creditor> filteredCreditors = creditors.stream().filter(x -> BigDecimal.valueOf(Double.valueOf(x.getAmount())).compareTo(BigDecimal.ZERO) != 0).collect(Collectors.toSet());
+ final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(filteredDebtors, filteredCreditors, productIdentifier, caseIdentifier, action);
+ Mockito.verify(ledgerManager, Mockito.atLeastOnce()).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
}
}
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/portfolio/Fixture.java b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
index 69e1c0b..de8fc57 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -139,9 +139,8 @@
ret.setCustomerIdentifier(CUSTOMER_IDENTIFIER);
ret.setMaximumBalance(fixScale(BigDecimal.valueOf(2000L)));
- ret.setTermRange(new TermRange(ChronoUnit.MONTHS, 18));
+ ret.setTermRange(new TermRange(ChronoUnit.MONTHS, 3));
ret.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 1, 1, null, null));
- ret.setPaymentSize(BigDecimal.ONE.negate().setScale(4, RoundingMode.HALF_EVEN));
final CreditWorthinessSnapshot customerCreditWorthinessSnapshot = new CreditWorthinessSnapshot();
customerCreditWorthinessSnapshot.setForCustomer("alice");
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
index b34a0ed..d2c78d2 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -41,10 +41,11 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
+import java.util.stream.IntStream;
import static io.mifos.portfolio.Fixture.MINOR_CURRENCY_UNIT_DIGITS;
@@ -89,6 +90,8 @@
@Test
public void workflowTerminatingInEarlyLoanPayoff() throws InterruptedException {
+ final LocalDateTime today = midnightToday();
+
step1CreateProduct();
step2CreateCase();
step3OpenCase();
@@ -96,13 +99,15 @@
step5Disburse(
BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
- step6CalculateInterestAccrual();
- step7PaybackPartialAmount(expectedCurrentBalance);
+ step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), null);
+ step7PaybackPartialAmount(expectedCurrentBalance, today, 0);
step8Close();
}
@Test
public void workflowWithTwoUnequalDisbursals() throws InterruptedException {
+ final LocalDateTime today = midnightToday();
+
step1CreateProduct();
step2CreateCase();
step3OpenCase();
@@ -113,13 +118,15 @@
step5Disburse(
BigDecimal.valueOf(1_500_00, MINOR_CURRENCY_UNIT_DIGITS),
UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(15_00, MINOR_CURRENCY_UNIT_DIGITS));
- step6CalculateInterestAccrual();
- step7PaybackPartialAmount(expectedCurrentBalance);
+ step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), null);
+ step7PaybackPartialAmount(expectedCurrentBalance, today, 0);
step8Close();
}
@Test
public void workflowWithTwoNearlyEqualRepayments() throws InterruptedException {
+ final LocalDateTime today = midnightToday();
+
step1CreateProduct();
step2CreateCase();
step3OpenCase();
@@ -127,10 +134,13 @@
step5Disburse(
BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
- step6CalculateInterestAccrual();
+ step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), null);
final BigDecimal repayment1 = expectedCurrentBalance.divide(BigDecimal.valueOf(2), BigDecimal.ROUND_HALF_EVEN);
- step7PaybackPartialAmount(repayment1.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
- step7PaybackPartialAmount(expectedCurrentBalance);
+ step7PaybackPartialAmount(
+ repayment1.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN),
+ today,
+ 0);
+ step7PaybackPartialAmount(expectedCurrentBalance, today, 0);
step8Close();
}
@@ -148,6 +158,56 @@
catch (IllegalArgumentException ignored) { }
}
+ @Test
+ public void workflowWithNormalRepayment() throws InterruptedException {
+ final LocalDateTime today = midnightToday();
+
+ step1CreateProduct();
+ step2CreateCase();
+ step3OpenCase();
+ step4ApproveCase();
+ step5Disburse(
+ BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
+ UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
+
+ int week = 0;
+ final List<BigDecimal> repayments = new ArrayList<>();
+ while (expectedCurrentBalance.compareTo(BigDecimal.ZERO) > 0) {
+ logger.info("Simulating week {}. Expected current balance {}.", week, expectedCurrentBalance);
+ step6CalculateInterestAndCheckForLatenessForWeek(today, week, null);
+ final BigDecimal nextRepaymentAmount = findNextRepaymentAmount(today, (week+1)*7);
+ repayments.add(nextRepaymentAmount);
+ step7PaybackPartialAmount(nextRepaymentAmount, today, (week+1)*7);
+ week++;
+ }
+
+ final BigDecimal minPayment = repayments.stream().min(BigDecimal::compareTo).orElseThrow(IllegalStateException::new);
+ final BigDecimal maxPayment = repayments.stream().max(BigDecimal::compareTo).orElseThrow(IllegalStateException::new);
+ final BigDecimal delta = maxPayment.subtract(minPayment).abs();
+ Assert.assertTrue("minPayment is " + minPayment + ", maxPayment is " + maxPayment,
+ delta.divide(maxPayment, BigDecimal.ROUND_HALF_EVEN).compareTo(BigDecimal.valueOf(0.01)) <= 0);
+
+
+ step8Close();
+ }
+
+ private BigDecimal findNextRepaymentAmount(
+ final LocalDateTime referenceDate,
+ final int dayNumber) {
+ AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
+
+ final List<CostComponent> costComponentsForNextPayment = portfolioManager.getCostComponentsForAction(
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ Action.ACCEPT_PAYMENT.name(),
+ null,
+ null,
+ DateConverter.toIsoString(referenceDate.plusDays(dayNumber)));
+ return costComponentsForNextPayment.stream().filter(x -> x.getChargeIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID)).findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("return missing repayment charge."))
+ .getAmount();
+ }
+
//Create product and set charges to fixed fees.
private void step1CreateProduct() throws InterruptedException {
logger.info("step1CreateProduct");
@@ -204,8 +264,9 @@
private void step2CreateCase() throws InterruptedException {
logger.info("step2CreateCase");
- caseParameters = Fixture.createAdjustedCaseParameters(x -> {
- });
+ caseParameters = Fixture.createAdjustedCaseParameters(x ->
+ x.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, null, null, null))
+ );
final String caseParametersAsString = new Gson().toJson(caseParameters);
customerCase = createAdjustedCase(product.getIdentifier(), x -> x.setParameters(caseParametersAsString));
@@ -314,6 +375,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE,
+ LocalDateTime.now(Clock.systemUTC()),
Collections.singletonList(assignEntryToTeller()),
amount,
IndividualLoanEventConstants.DISBURSE_INDIVIDUALLOAN_CASE,
@@ -337,17 +399,44 @@
expectedCurrentBalance = expectedCurrentBalance.add(amount);
}
+ private void step6CalculateInterestAndCheckForLatenessForWeek(
+ final LocalDateTime referenceDate,
+ final int weekNumber,
+ final BigDecimal calculatedLateFee) throws InterruptedException {
+ try {
+ IntStream.rangeClosed((weekNumber*7)+1, (weekNumber+1)*7)
+ .mapToObj(referenceDate::plusDays)
+ .forEach(day -> {
+ try {
+ step6CalculateInterestAccrualAndCheckForLateness(day, calculatedLateFee);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+ catch (RuntimeException e) {
+ final Throwable cause = e.getCause();
+ if (cause != null && cause.getClass().isAssignableFrom(InterruptedException.class))
+ throw (InterruptedException)e.getCause();
+ else
+ throw e;
+ }
+ }
+
//Perform daily interest calculation.
- private void step6CalculateInterestAccrual() throws InterruptedException {
- logger.info("step6CalculateInterestAccrual");
+ private void step6CalculateInterestAccrualAndCheckForLateness(
+ final LocalDateTime forTime,
+ final BigDecimal calculatedLateFee) throws InterruptedException {
+ logger.info("step6CalculateInterestAccrualAndCheckForLateness");
final String beatIdentifier = "alignment0";
- final String midnightTimeStamp = DateConverter.toIsoString(midnightToday());
+ final String midnightTimeStamp = DateConverter.toIsoString(forTime);
AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
final BigDecimal calculatedInterest = expectedCurrentBalance.multiply(Fixture.INTEREST_RATE.divide(Fixture.ACCRUAL_PERIODS, 8, BigDecimal.ROUND_HALF_EVEN))
.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
+
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
@@ -355,11 +444,24 @@
Collections.singleton(AccountDesignators.CUSTOMER_LOAN),
null,
new CostComponent(ChargeIdentifiers.INTEREST_ID, calculatedInterest));
+
+ if (calculatedLateFee != null) {
+ checkCostComponentForActionCorrect(
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ Action.MARK_LATE,
+ Collections.singleton(AccountDesignators.CUSTOMER_LOAN),
+ null,
+ new CostComponent(ChargeIdentifiers.LATE_FEE_ID, calculatedLateFee));
+ }
final BeatPublish interestBeat = new BeatPublish(beatIdentifier, midnightTimeStamp);
portfolioBeatListener.publishBeat(interestBeat);
Assert.assertTrue(this.eventRecorder.wait(io.mifos.rhythm.spi.v1.events.EventConstants.POST_PUBLISHEDBEAT,
new BeatPublishEvent(EventConstants.DESTINATION, beatIdentifier, midnightTimeStamp)));
+ Assert.assertTrue(this.eventRecorder.wait(IndividualLoanEventConstants.CHECK_LATE_INDIVIDUALLOAN_CASE,
+ new IndividualLoanCommandEvent(product.getIdentifier(), customerCase.getIdentifier(), midnightTimeStamp)));
+
Assert.assertTrue(eventRecorder.wait(IndividualLoanEventConstants.APPLY_INTEREST_INDIVIDUALLOAN_CASE,
new IndividualLoanCommandEvent(product.getIdentifier(), customerCase.getIdentifier(), midnightTimeStamp)));
@@ -384,7 +486,10 @@
expectedCurrentBalance = expectedCurrentBalance.add(calculatedInterest);
}
- private void step7PaybackPartialAmount(final BigDecimal amount) throws InterruptedException {
+ private void step7PaybackPartialAmount(
+ final BigDecimal amount,
+ final LocalDateTime referenceDate,
+ final int dayNumber) throws InterruptedException {
logger.info("step7PaybackPartialAmount '{}'", amount);
AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
@@ -404,6 +509,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.ACCEPT_PAYMENT,
+ referenceDate.plusDays(dayNumber),
Collections.singletonList(assignEntryToTeller()),
amount,
IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
diff --git a/component-test/src/main/java/io/mifos/portfolio/listener/IndividualLoanCaseCommandEventListener.java b/component-test/src/main/java/io/mifos/portfolio/listener/IndividualLoanCaseCommandEventListener.java
index 1e4d4d3..e198db2 100644
--- a/component-test/src/main/java/io/mifos/portfolio/listener/IndividualLoanCaseCommandEventListener.java
+++ b/component-test/src/main/java/io/mifos/portfolio/listener/IndividualLoanCaseCommandEventListener.java
@@ -99,9 +99,19 @@
}
@JmsListener(
- subscription = IndividualLoanEventConstants.DESTINATION,
- destination = IndividualLoanEventConstants.DESTINATION,
- selector = IndividualLoanEventConstants.SELECTOR_MARK_LATE_INDIVIDUALLOAN_CASE
+ subscription = IndividualLoanEventConstants.DESTINATION,
+ destination = IndividualLoanEventConstants.DESTINATION,
+ selector = IndividualLoanEventConstants.SELECTOR_CHECK_LATE_INDIVIDUALLOAN_CASE
+ )
+ public void onCheckLate(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+ final String payload) {
+ this.eventRecorder.event(tenant, IndividualLoanEventConstants.CHECK_LATE_INDIVIDUALLOAN_CASE, payload, IndividualLoanCommandEvent.class);
+ }
+
+ @JmsListener(
+ subscription = IndividualLoanEventConstants.DESTINATION,
+ destination = IndividualLoanEventConstants.DESTINATION,
+ selector = IndividualLoanEventConstants.SELECTOR_MARK_LATE_INDIVIDUALLOAN_CASE
)
public void onMarkLate(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
final String payload) {
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index cb83144..8fd47db 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -20,7 +20,6 @@
import io.mifos.core.lang.ServiceException;
import io.mifos.customer.api.v1.client.CustomerManager;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
-import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
@@ -44,9 +43,11 @@
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
+import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.*;
import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
@@ -168,13 +169,13 @@
//TODO: Make payable at time of ACCEPT_PAYMENT but accrued at MARK_LATE
final ChargeDefinition lateFee = charge(
LATE_FEE_NAME,
- Action.MARK_LATE,
- BigDecimal.ONE,
+ Action.ACCEPT_PAYMENT,
+ BigDecimal.TEN,
CUSTOMER_LOAN,
LATE_FEE_INCOME);
lateFee.setAccrueAction(Action.MARK_LATE.name());
lateFee.setAccrualAccountDesignator(LATE_FEE_ACCRUAL);
- lateFee.setProportionalTo(ChargeIdentifiers.REPAYMENT_ID);
+ lateFee.setProportionalTo(ChargeProportionalDesignator.REPAYMENT_DESIGNATOR.getValue());
lateFee.setReadOnly(false);
//TODO: Make multiple write off allowance charges.
@@ -347,6 +348,7 @@
final String productIdentifier,
final String caseIdentifier,
final String actionIdentifier,
+ final LocalDateTime forDateTime,
final Set<String> forAccountDesignators,
final BigDecimal forPaymentSize) {
final Action action = Action.valueOf(actionIdentifier);
@@ -354,14 +356,21 @@
final Case.State caseState = Case.State.valueOf(dataContextOfAction.getCustomerCaseEntity().getCurrentState());
checkActionCanBeExecuted(caseState, action);
- return costComponentService.getCostComponentsForAction(action, dataContextOfAction, forPaymentSize)
- .stream()
- .filter(costComponentEntry -> chargeReferencesAccountDesignators(costComponentEntry.getKey(), action, forAccountDesignators))
+ Stream<Map.Entry<ChargeDefinition, CostComponent>> costComponentStream = costComponentService.getCostComponentsForAction(
+ action,
+ dataContextOfAction,
+ forPaymentSize,
+ forDateTime.toLocalDate())
+ .stream();
+
+ if (!forAccountDesignators.isEmpty()) {
+ costComponentStream = costComponentStream
+ .filter(costComponentEntry -> chargeReferencesAccountDesignators(costComponentEntry.getKey(), action, forAccountDesignators));
+ }
+
+ return costComponentStream
.map(costComponentEntry -> new CostComponent(costComponentEntry.getKey().getIdentifier(), costComponentEntry.getValue().getAmount()))
- .collect(Collectors.toList())
- .stream()
- .map(x -> new CostComponent(x.getChargeIdentifier(), x.getAmount()))
- .collect(Collectors.toList());
+ .collect(Collectors.toList());
}
private boolean chargeReferencesAccountDesignators(
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/CheckLateCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/CheckLateCommand.java
index bb31db2..acab032 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/CheckLateCommand.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/CheckLateCommand.java
@@ -15,6 +15,9 @@
*/
package io.mifos.individuallending.internal.command;
+/**
+ * @author Myrle Krantz
+ */
public class CheckLateCommand {
private final String productIdentifier;
private final String caseIdentifier;
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/MarkLateCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/MarkLateCommand.java
new file mode 100644
index 0000000..400a8ba
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/MarkLateCommand.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.individuallending.internal.command;
+
+/**
+ * @author Myrle Krantz
+ */
+public class MarkLateCommand {
+ private final String productIdentifier;
+ private final String caseIdentifier;
+ private final String forTime;
+
+ public MarkLateCommand(String productIdentifier, String caseIdentifier, String forTime) {
+ this.productIdentifier = productIdentifier;
+ this.caseIdentifier = caseIdentifier;
+ this.forTime = forTime;
+ }
+
+ public String getProductIdentifier() {
+ return productIdentifier;
+ }
+
+ public String getCaseIdentifier() {
+ return caseIdentifier;
+ }
+
+ public String getForTime() {
+ return forTime;
+ }
+
+ @Override
+ public String toString() {
+ return "MarkLateCommand{" +
+ "productIdentifier='" + productIdentifier + '\'' +
+ ", caseIdentifier='" + caseIdentifier + '\'' +
+ ", forTime='" + forTime + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
index 8f1c6e9..a9cc03a 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
@@ -22,23 +22,31 @@
import io.mifos.core.command.internal.CommandBus;
import io.mifos.core.lang.ApplicationName;
import io.mifos.core.lang.DateConverter;
+import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
import io.mifos.individuallending.internal.command.ApplyInterestCommand;
import io.mifos.individuallending.internal.command.CheckLateCommand;
+import io.mifos.individuallending.internal.command.MarkLateCommand;
import io.mifos.individuallending.internal.service.DataContextOfAction;
import io.mifos.individuallending.internal.service.DataContextService;
+import io.mifos.individuallending.internal.service.DesignatorToAccountIdentifierMapper;
+import io.mifos.individuallending.internal.service.ScheduledActionHelpers;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.service.config.PortfolioProperties;
import io.mifos.portfolio.service.internal.command.CreateBeatPublishCommand;
import io.mifos.portfolio.service.internal.repository.CaseEntity;
import io.mifos.portfolio.service.internal.repository.CaseRepository;
+import io.mifos.portfolio.service.internal.util.AccountingAdapter;
import io.mifos.rhythm.spi.v1.domain.BeatPublish;
import io.mifos.rhythm.spi.v1.events.BeatPublishEvent;
import io.mifos.rhythm.spi.v1.events.EventConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
+import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.stream.Stream;
@@ -54,6 +62,7 @@
private final DataContextService dataContextService;
private final ApplicationName applicationName;
private final CommandBus commandBus;
+ private final AccountingAdapter accountingAdapter;
@Autowired
public BeatPublishCommandHandler(
@@ -61,12 +70,14 @@
final PortfolioProperties portfolioProperties,
final DataContextService dataContextService,
final ApplicationName applicationName,
- final CommandBus commandBus) {
+ final CommandBus commandBus,
+ final AccountingAdapter accountingAdapter) {
this.caseRepository = caseRepository;
this.portfolioProperties = portfolioProperties;
this.dataContextService = dataContextService;
this.applicationName = applicationName;
this.commandBus = commandBus;
+ this.accountingAdapter = accountingAdapter;
}
@Transactional
@@ -106,14 +117,53 @@
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(
selectorName = io.mifos.portfolio.api.v1.events.EventConstants.SELECTOR_NAME,
- selectorValue = IndividualLoanEventConstants.SELECTOR_CHECK_LATE_INDIVIDUALLOAN_CASE)
+ selectorValue = IndividualLoanEventConstants.CHECK_LATE_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final CheckLateCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
+ final LocalDateTime forTime = DateConverter.fromIsoString(command.getForTime());
final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
productIdentifier, caseIdentifier, Collections.emptyList());
-//TODO:
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
+ final String lateFeeAccrualAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.LATE_FEE_ACCRUAL);
+
+ final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+ if (currentBalance.compareTo(BigDecimal.ZERO) == 0) //No late fees if the current balance is zilch.
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getForTime());
+
+
+ final LocalDateTime dateOfMostRecentDisbursement =
+ accountingAdapter.getDateOfMostRecentEntryContainingMessage(customerLoanAccountIdentifier, dataContextOfAction.getMessageForCharge(Action.DISBURSE))
+ .orElseThrow(() ->
+ ServiceException.badRequest("No last disbursal date for ''{0}.{1}'' could be determined. " +
+ "Therefore it cannot be checked for lateness.", productIdentifier, caseIdentifier));
+
+ final long repaymentPeriodsBetweenBeginningAndToday = ScheduledActionHelpers.generateRepaymentPeriods(
+ dateOfMostRecentDisbursement.toLocalDate(),
+ forTime.toLocalDate(),
+ dataContextOfAction.getCaseParameters())
+ .count() - 1;
+
+ final BigDecimal expectedPaymentSum = dataContextOfAction
+ .getCaseParametersEntity()
+ .getPaymentSize()
+ .multiply(BigDecimal.valueOf(repaymentPeriodsBetweenBeginningAndToday));
+
+ final BigDecimal paymentsSum = accountingAdapter.sumMatchingEntriesSinceDate(
+ customerLoanAccountIdentifier,
+ dateOfMostRecentDisbursement.toLocalDate(),
+ dataContextOfAction.getMessageForCharge(Action.ACCEPT_PAYMENT));
+
+ final BigDecimal lateFeesAccrued = accountingAdapter.sumMatchingEntriesSinceDate(
+ lateFeeAccrualAccountIdentifier,
+ dateOfMostRecentDisbursement.toLocalDate(),
+ dataContextOfAction.getMessageForCharge(Action.MARK_LATE));
+
+ if (paymentsSum.compareTo(expectedPaymentSum.add(lateFeesAccrued)) < 0)
+ commandBus.dispatch(new MarkLateCommand(productIdentifier, caseIdentifier, command.getForTime()));
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getForTime());
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
index 5d105b4..dd0822d 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
@@ -24,7 +24,6 @@
import io.mifos.core.lang.ServiceException;
import io.mifos.individuallending.IndividualLendingPatternFactory;
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
-import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
@@ -338,12 +337,10 @@
"End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- costComponentService.getCostComponentsForAcceptPayment(dataContextOfAction, command.getCommand().getPaymentSize());
-
- final BigDecimal sumOfAdjustments = costComponentsForRepaymentPeriod.stream()
- .filter(entry -> entry.getKey().getIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID))
- .map(entry -> entry.getValue().getAmount())
- .reduce(BigDecimal.ZERO, BigDecimal::add);
+ costComponentService.getCostComponentsForAcceptPayment(
+ dataContextOfAction,
+ command.getCommand().getPaymentSize(),
+ DateConverter.fromIsoString(command.getCommand().getCreatedOn()).toLocalDate());
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
@@ -369,6 +366,49 @@
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ @EventEmitter(
+ selectorName = EventConstants.SELECTOR_NAME,
+ selectorValue = IndividualLoanEventConstants.MARK_LATE_INDIVIDUALLOAN_CASE)
+ public IndividualLoanCommandEvent process(final MarkLateCommand command) {
+ final String productIdentifier = command.getProductIdentifier();
+ final String caseIdentifier = command.getCaseIdentifier();
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
+ productIdentifier, caseIdentifier, Collections.emptyList());
+ IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCaseEntity().getCurrentState()), Action.MARK_LATE);
+
+ checkIfTasksAreOutstanding(dataContextOfAction, Action.MARK_LATE);
+
+ if (dataContextOfAction.getCustomerCaseEntity().getEndOfTerm() == null)
+ throw ServiceException.internalError(
+ "End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
+
+ final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ costComponentService.getCostComponentsForMarkLate(dataContextOfAction, DateConverter.fromIsoString(command.getForTime()));
+
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+
+ final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
+ .map(entry -> mapCostComponentEntryToChargeInstance(
+ Action.MARK_LATE,
+ entry,
+ designatorToAccountIdentifierMapper))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toList());
+
+ final LocalDateTime today = today();
+
+ accountingAdapter.bookCharges(charges,
+ "Marked late on " + command.getForTime(),
+ dataContextOfAction.getMessageForCharge(Action.MARK_LATE),
+ Action.MARK_LATE.getTransactionType());
+
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
+ }
+
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.WRITE_OFF_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final WriteOffCommand command) {
final String productIdentifier = command.getProductIdentifier();
@@ -503,7 +543,7 @@
action.name(), productIdentifier, caseIdentifier);
}
- private LocalDateTime today() {
+ private static LocalDateTime today() {
return LocalDate.now(Clock.systemUTC()).atStartOfDay();
}
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/mapper/CaseParametersMapper.java b/service/src/main/java/io/mifos/individuallending/internal/mapper/CaseParametersMapper.java
index c1231f1..bac7bdd 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/mapper/CaseParametersMapper.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/mapper/CaseParametersMapper.java
@@ -149,7 +149,6 @@
ret.setTermRange(getTermRange(caseParametersEntity));
ret.setMaximumBalance(caseParametersEntity.getBalanceRangeMaximum().setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN));
ret.setPaymentCycle(getPaymentCycle(caseParametersEntity));
- ret.setPaymentSize(caseParametersEntity.getPaymentSize());
return ret;
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
index 5f73add..551c39e 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
@@ -61,7 +61,8 @@
public CostComponentsForRepaymentPeriod getCostComponentsForAction(
final Action action,
final DataContextOfAction dataContextOfAction,
- final BigDecimal forPaymentSize) {
+ final BigDecimal forPaymentSize,
+ final LocalDate forDate) {
switch (action) {
case OPEN:
return getCostComponentsForOpen(dataContextOfAction);
@@ -74,11 +75,11 @@
case APPLY_INTEREST:
return getCostComponentsForApplyInterest(dataContextOfAction);
case ACCEPT_PAYMENT:
- return getCostComponentsForAcceptPayment(dataContextOfAction, forPaymentSize);
+ return getCostComponentsForAcceptPayment(dataContextOfAction, forPaymentSize, forDate);
case CLOSE:
return getCostComponentsForClose(dataContextOfAction);
case MARK_LATE:
- return getCostComponentsForMarkLate(dataContextOfAction);
+ return getCostComponentsForMarkLate(dataContextOfAction, today().atStartOfDay());
case WRITE_OFF:
return getCostComponentsForWriteOff(dataContextOfAction);
case RECOVER:
@@ -245,7 +246,8 @@
public CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(
final DataContextOfAction dataContextOfAction,
- final @Nullable BigDecimal requestedLoanPaymentSize)
+ final @Nullable BigDecimal requestedLoanPaymentSize,
+ final LocalDate forDate)
{
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
@@ -260,14 +262,24 @@
final ScheduledAction scheduledAction
= ScheduledActionHelpers.getNextScheduledPayment(
startOfTerm,
+ forDate,
dataContextOfAction.getCustomerCaseEntity().getEndOfTerm().toLocalDate(),
dataContextOfAction.getCaseParameters()
);
- final BigDecimal loanPaymentSize =
- (requestedLoanPaymentSize != null) ?
- requestedLoanPaymentSize :
- dataContextOfAction.getCaseParametersEntity().getPaymentSize();
+ final BigDecimal loanPaymentSize;
+
+ if (requestedLoanPaymentSize != null) {
+ loanPaymentSize = requestedLoanPaymentSize;
+ }
+ else {
+ if (scheduledAction.actionPeriod != null && scheduledAction.actionPeriod.isLastPeriod()) {
+ loanPaymentSize = currentBalance;
+ }
+ else {
+ loanPaymentSize = currentBalance.min(dataContextOfAction.getCaseParametersEntity().getPaymentSize());
+ }
+ }
final List<ScheduledCharge> scheduledChargesForThisAction = scheduledChargesService.getScheduledCharges(
productIdentifier,
@@ -295,50 +307,6 @@
true);
}
- private static boolean isAccruedChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
- return chargeDefinition.getAccrueAction() != null &&
- chargeDefinition.getChargeAction().equals(action.name());
- }
-
- private static boolean isAccrualChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
- return chargeDefinition.getAccrueAction() != null &&
- chargeDefinition.getAccrueAction().equals(action.name());
- }
-
- private CostComponent getAccruedCostComponentToApply(final DataContextOfAction dataContextOfAction,
- final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper,
- final LocalDate startOfTerm,
- final ChargeDefinition chargeDefinition) {
- final CostComponent ret = new CostComponent();
-
- final String accrualAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator());
-
- final BigDecimal amountAccrued = accountingAdapter.sumMatchingEntriesSinceDate(
- accrualAccountIdentifier,
- startOfTerm,
- dataContextOfAction.getMessageForCharge(Action.valueOf(chargeDefinition.getAccrueAction())));
- final BigDecimal amountApplied = accountingAdapter.sumMatchingEntriesSinceDate(
- accrualAccountIdentifier,
- startOfTerm,
- dataContextOfAction.getMessageForCharge(Action.valueOf(chargeDefinition.getChargeAction())));
-
- ret.setChargeIdentifier(chargeDefinition.getIdentifier());
- ret.setAmount(amountAccrued.subtract(amountApplied));
- return ret;
- }
-
- private LocalDate getStartOfTermOrThrow(final DataContextOfAction dataContextOfAction,
- final String customerLoanAccountIdentifier) {
- final Optional<LocalDateTime> firstDisbursalDateTime = accountingAdapter.getDateOfOldestEntryContainingMessage(
- customerLoanAccountIdentifier,
- dataContextOfAction.getMessageForCharge(Action.DISBURSE));
-
- return firstDisbursalDateTime.map(LocalDateTime::toLocalDate)
- .orElseThrow(() -> ServiceException.internalError(
- "Start of term for loan ''{0}'' could not be acquired from accounting.",
- dataContextOfAction.getCompoundIdentifer()));
- }
-
public CostComponentsForRepaymentPeriod getCostComponentsForClose(final DataContextOfAction dataContextOfAction) {
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
@@ -379,8 +347,45 @@
true);
}
- private CostComponentsForRepaymentPeriod getCostComponentsForMarkLate(final DataContextOfAction dataContextOfAction) {
- return null;
+ public CostComponentsForRepaymentPeriod getCostComponentsForMarkLate(final DataContextOfAction dataContextOfAction,
+ final LocalDateTime forTime) {
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
+ final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
+
+ final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
+
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+ final ScheduledAction scheduledAction = new ScheduledAction(Action.MARK_LATE, forTime.toLocalDate());
+
+ final BigDecimal loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize();
+
+ final List<ScheduledCharge> scheduledChargesForThisAction = scheduledChargesService.getScheduledCharges(
+ productIdentifier,
+ Collections.singletonList(scheduledAction));
+
+ final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledChargesForThisAction.stream()
+ .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x.getChargeDefinition(), Action.MARK_LATE)));
+
+ final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
+ .stream()
+ .map(ScheduledCharge::getChargeDefinition)
+ .collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
+ chargeDefinition -> getAccruedCostComponentToApply(dataContextOfAction, designatorToAccountIdentifierMapper, startOfTerm, chargeDefinition)));
+
+
+ return getCostComponentsForScheduledCharges(
+ accruedCostComponents,
+ chargesSplitIntoScheduledAndAccrued.get(false),
+ caseParameters.getBalanceRangeMaximum(),
+ currentBalance,
+ loanPaymentSize,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
}
private CostComponentsForRepaymentPeriod getCostComponentsForWriteOff(final DataContextOfAction dataContextOfAction) {
@@ -482,6 +487,8 @@
case REPAYMENT_DESIGNATOR:
return loanPaymentSize;
case PRINCIPAL_ADJUSTMENT_DESIGNATOR: {
+ if (loanPaymentSize.compareTo(BigDecimal.ZERO) <= 0)
+ return loanPaymentSize.abs();
final BigDecimal newRunningBalance
= runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
final BigDecimal newLoanPaymentSize = loanPaymentSize.min(newRunningBalance);
@@ -599,6 +606,50 @@
return chargeDefinition.getAccrualAccountDesignator() != null;
}
+ private static boolean isAccruedChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
+ return chargeDefinition.getAccrueAction() != null &&
+ chargeDefinition.getChargeAction().equals(action.name());
+ }
+
+ private static boolean isAccrualChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
+ return chargeDefinition.getAccrueAction() != null &&
+ chargeDefinition.getAccrueAction().equals(action.name());
+ }
+
+ private CostComponent getAccruedCostComponentToApply(final DataContextOfAction dataContextOfAction,
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper,
+ final LocalDate startOfTerm,
+ final ChargeDefinition chargeDefinition) {
+ final CostComponent ret = new CostComponent();
+
+ final String accrualAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator());
+
+ final BigDecimal amountAccrued = accountingAdapter.sumMatchingEntriesSinceDate(
+ accrualAccountIdentifier,
+ startOfTerm,
+ dataContextOfAction.getMessageForCharge(Action.valueOf(chargeDefinition.getAccrueAction())));
+ final BigDecimal amountApplied = accountingAdapter.sumMatchingEntriesSinceDate(
+ accrualAccountIdentifier,
+ startOfTerm,
+ dataContextOfAction.getMessageForCharge(Action.valueOf(chargeDefinition.getChargeAction())));
+
+ ret.setChargeIdentifier(chargeDefinition.getIdentifier());
+ ret.setAmount(amountAccrued.subtract(amountApplied));
+ return ret;
+ }
+
+ private LocalDate getStartOfTermOrThrow(final DataContextOfAction dataContextOfAction,
+ final String customerLoanAccountIdentifier) {
+ final Optional<LocalDateTime> firstDisbursalDateTime = accountingAdapter.getDateOfOldestEntryContainingMessage(
+ customerLoanAccountIdentifier,
+ dataContextOfAction.getMessageForCharge(Action.DISBURSE));
+
+ return firstDisbursalDateTime.map(LocalDateTime::toLocalDate)
+ .orElseThrow(() -> ServiceException.internalError(
+ "Start of term for loan ''{0}'' could not be acquired from accounting.",
+ dataContextOfAction.getCompoundIdentifer()));
+ }
+
private static LocalDate today() {
return LocalDate.now(Clock.systemUTC());
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
index cb7938a..83372f4 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
@@ -41,7 +41,8 @@
}
public Stream<Map.Entry<ChargeDefinition, CostComponent>> stream() {
- return costComponents.entrySet().stream();
+ return costComponents.entrySet().stream()
+ .filter(costComponentEntry -> costComponentEntry.getValue().getAmount().compareTo(BigDecimal.ZERO) != 0);
}
BigDecimal getBalanceAdjustment() {
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java b/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
index 6108651..62ddc51 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
@@ -15,9 +15,11 @@
*/
package io.mifos.individuallending.internal.service;
+import io.mifos.core.lang.ServiceException;
import io.mifos.individuallending.api.v1.domain.caseinstance.ChargeName;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -49,6 +51,8 @@
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(initialDisbursalDate, dataContextOfAction.getCaseParameters());
+ final Set<Action> actionsScheduled = scheduledActions.stream().map(x -> x.action).collect(Collectors.toSet());
+
final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(dataContextOfAction.getProductEntity().getIdentifier(), scheduledActions);
final BigDecimal loanPaymentSize = CostComponentService.getLoanPaymentSize(
@@ -60,6 +64,7 @@
final List<PlannedPayment> plannedPaymentsElements = getPlannedPaymentsElements(
dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum(),
minorCurrencyUnitDigits,
+ actionsScheduled,
scheduledCharges,
loanPaymentSize,
dataContextOfAction.getInterest());
@@ -78,6 +83,8 @@
final Set<ChargeName> chargeNames) {
final int fromIndex = size*pageIndex;
final int toIndex = Math.min(size*(pageIndex+1), plannedPaymentsElements.size());
+ if (toIndex < fromIndex)
+ throw ServiceException.badRequest("Page number ''{0}'' out of range.", pageIndex);
final List<PlannedPayment> elements = plannedPaymentsElements.subList(fromIndex, toIndex);
final PlannedPaymentPage ret = new PlannedPaymentPage();
@@ -97,15 +104,17 @@
static private List<PlannedPayment> getPlannedPaymentsElements(
final BigDecimal initialBalance,
final int minorCurrencyUnitDigits,
+ final Set<Action> actionsScheduled,
final List<ScheduledCharge> scheduledCharges,
final BigDecimal loanPaymentSize,
final BigDecimal interest) {
final Map<Period, SortedSet<ScheduledCharge>> orderedScheduledChargesGroupedByPeriod
= scheduledCharges.stream()
+ .filter(scheduledCharge -> chargeIsNotAccruedOrAccruesAtActionScheduled(actionsScheduled, scheduledCharge))
.collect(Collectors.groupingBy(IndividualLoanService::getPeriodFromScheduledCharge,
Collectors.mapping(x -> x,
Collector.of(
- () -> new TreeSet<>(new ScheduledChargesService.ScheduledChargeComparator()),
+ () -> new TreeSet<>(new ScheduledChargeComparator()),
SortedSet::add,
(left, right) -> { left.addAll(right); return left; }))));
@@ -153,6 +162,14 @@
return plannedPayments;
}
+ private static boolean chargeIsNotAccruedOrAccruesAtActionScheduled(
+ final Set<Action> actionsScheduled,
+ final ScheduledCharge scheduledCharge) {
+ // For example to prevent late charges from showing up on planned payments.
+ return scheduledCharge.getChargeDefinition().getAccrueAction() == null ||
+ actionsScheduled.contains(Action.valueOf(scheduledCharge.getChargeDefinition().getAccrueAction()));
+ }
+
private static Period getPeriodFromScheduledCharge(final ScheduledCharge scheduledCharge) {
final ScheduledAction scheduledAction = scheduledCharge.getScheduledAction();
if (ScheduledActionHelpers.actionHasNoActionPeriod(scheduledAction.action))
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/Period.java b/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
index 2b28eb5..625f6cf 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
@@ -29,20 +29,30 @@
class Period implements Comparable<Period> {
final private LocalDate beginDate;
final private LocalDate endDate;
+ final private boolean lastPeriod;
Period(final LocalDate beginDate, final LocalDate endDateExclusive) {
this.beginDate = beginDate;
this.endDate = endDateExclusive;
+ this.lastPeriod = false;
+ }
+
+ Period(final LocalDate beginDate, final LocalDate endDateExclusive, final boolean lastPeriod) {
+ this.beginDate = beginDate;
+ this.endDate = endDateExclusive;
+ this.lastPeriod = lastPeriod;
}
Period(final LocalDate beginDate, final int periodLength) {
this.beginDate = beginDate;
this.endDate = beginDate.plusDays(periodLength);
+ this.lastPeriod = false;
}
Period(final int periodLength, final LocalDate endDate) {
this.beginDate = endDate.minusDays(periodLength);
this.endDate = endDate;
+ this.lastPeriod = false;
}
LocalDate getBeginDate() {
@@ -53,6 +63,10 @@
return endDate;
}
+ boolean isLastPeriod() {
+ return lastPeriod;
+ }
+
String getEndDateAsString() {
return endDate == null ? null : DateConverter.toIsoString(endDate);
}
@@ -74,24 +88,33 @@
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- Period that = (Period) o;
- return Objects.equals(beginDate, that.beginDate) &&
- Objects.equals(endDate, that.endDate);
+ Period period = (Period) o;
+ return lastPeriod == period.lastPeriod &&
+ Objects.equals(beginDate, period.beginDate) &&
+ Objects.equals(endDate, period.endDate);
}
@Override
public int hashCode() {
- return Objects.hash(beginDate, endDate);
+ return Objects.hash(beginDate, endDate, lastPeriod);
}
@Override
public int compareTo(@Nonnull Period o) {
- final int comparison = compareNullableDates(endDate, o.endDate);
-
- if (comparison == 0)
- return compareNullableDates(beginDate, o.beginDate);
- else
+ int comparison = compareNullableDates(endDate, o.endDate);
+ if (comparison != 0)
return comparison;
+
+ comparison = compareNullableDates(beginDate, o.beginDate);
+ if (comparison != 0)
+ return comparison;
+
+ if (lastPeriod == o.lastPeriod)
+ return 0;
+ else if (lastPeriod)
+ return -1;
+ else
+ return 1;
}
@SuppressWarnings("ConstantConditions")
@@ -109,8 +132,9 @@
@Override
public String toString() {
return "Period{" +
- "beginDate=" + beginDate +
- ", endDate=" + endDate +
- '}';
+ "beginDate=" + beginDate +
+ ", endDate=" + endDate +
+ ", lastPeriod=" + lastPeriod +
+ '}';
}
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
index 91281cc..3aaebf5 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
@@ -17,7 +17,6 @@
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
-import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.Duration;
@@ -31,13 +30,7 @@
/**
* @author Myrle Krantz
*/
-@SuppressWarnings("WeakerAccess")
-@Service
-public class PeriodChargeCalculator {
- public PeriodChargeCalculator()
- {
- }
-
+class PeriodChargeCalculator {
static Map<Period, BigDecimal> getPeriodAccrualInterestRate(
final BigDecimal interest,
final List<ScheduledCharge> scheduledCharges,
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java
index 323e3f1..edcef2e 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java
@@ -59,7 +59,7 @@
}
boolean actionIsOnOrAfter(final LocalDate date) {
- return when.compareTo(date) > 0;
+ return when.compareTo(date) >= 0;
}
@Override
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
index b9c5eb6..c6f4fc8 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
@@ -55,16 +55,16 @@
}
public static ScheduledAction getNextScheduledPayment(final @Nonnull LocalDate startOfTerm,
+ final @Nonnull LocalDate fromDate,
final @Nonnull LocalDate endOfTerm,
final @Nonnull CaseParameters caseParameters) {
- final LocalDate now = LocalDate.now(Clock.systemUTC());
- final LocalDate effectiveEndOfTerm = now.isAfter(endOfTerm) ? now : endOfTerm;
+ final LocalDate effectiveEndOfTerm = fromDate.isAfter(endOfTerm) ? fromDate : endOfTerm;
return getHypotheticalScheduledActionsForDisbursedLoan(startOfTerm, effectiveEndOfTerm, caseParameters)
.filter(x -> x.action.equals(Action.ACCEPT_PAYMENT))
- .filter(x -> x.actionIsOnOrAfter(now))
+ .filter(x -> x.actionIsOnOrAfter(fromDate))
.findFirst()
- .orElseGet(() -> new ScheduledAction(Action.ACCEPT_PAYMENT, now));
+ .orElseGet(() -> new ScheduledAction(Action.ACCEPT_PAYMENT, fromDate));
}
private static Stream<ScheduledAction> getHypotheticalScheduledActionsForDisbursedLoan(
@@ -104,10 +104,10 @@
.limit(ChronoUnit.DAYS.between(repaymentPeriod.getBeginDate(), repaymentPeriod.getEndDate()));
}
- private static Stream<Period> generateRepaymentPeriods(
- final LocalDate startOfTerm,
- final LocalDate endOfTerm,
- final CaseParameters caseParameters) {
+ public static Stream<Period> generateRepaymentPeriods(
+ final LocalDate startOfTerm,
+ final LocalDate endOfTerm,
+ final CaseParameters caseParameters) {
final List<Period> ret = new ArrayList<>();
LocalDate lastPaymentDate = startOfTerm;
@@ -119,7 +119,7 @@
lastPaymentDate = nextPaymentDate;
nextPaymentDate = generateNextPaymentDate(caseParameters, lastPaymentDate);
}
- ret.add(new Period(lastPaymentDate, nextPaymentDate));
+ ret.add(new Period(lastPaymentDate, nextPaymentDate, true));
return ret.stream();
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargeComparator.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargeComparator.java
new file mode 100644
index 0000000..e597b48
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargeComparator.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.individuallending.internal.service;
+
+
+import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+
+import java.util.Comparator;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+class ScheduledChargeComparator implements Comparator<ScheduledCharge>
+{
+ @Override
+ public int compare(ScheduledCharge o1, ScheduledCharge o2) {
+ return compareScheduledCharges(o1, o2);
+ }
+
+ static int compareScheduledCharges(ScheduledCharge o1, ScheduledCharge o2) {
+ int ret = o1.getScheduledAction().when.compareTo(o2.getScheduledAction().when);
+ if (ret != 0)
+ return ret;
+
+ ret = o1.getScheduledAction().action.compareTo(o2.getScheduledAction().action);
+ if (ret != 0)
+ return ret;
+
+ ret = proportionalityApplicationOrder(o1.getChargeDefinition(), o2.getChargeDefinition());
+ if (ret != 0)
+ return ret;
+
+ return o1.getChargeDefinition().getIdentifier().compareTo(o2.getChargeDefinition().getIdentifier());
+ }
+
+ static int proportionalityApplicationOrder(final ChargeDefinition o1, final ChargeDefinition o2) {
+ final Optional<ChargeProportionalDesignator> aProportionalToDesignator
+ = ChargeProportionalDesignator.fromString(o1.getProportionalTo());
+ final Optional<ChargeProportionalDesignator> bProportionalToDesignator
+ = ChargeProportionalDesignator.fromString(o2.getProportionalTo());
+
+ if (aProportionalToDesignator.isPresent() && bProportionalToDesignator.isPresent())
+ return Integer.compare(
+ aProportionalToDesignator.get().getOrderOfApplication(),
+ bProportionalToDesignator.get().getOrderOfApplication());
+ else if (aProportionalToDesignator.isPresent())
+ return 1;
+ else if (bProportionalToDesignator.isPresent())
+ return -1;
+ else
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargesService.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargesService.java
index fcbfc6c..b578443 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargesService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargesService.java
@@ -15,7 +15,6 @@
*/
package io.mifos.individuallending.internal.service;
-import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.service.internal.repository.BalanceSegmentEntity;
import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
@@ -103,6 +102,15 @@
Optional<BigDecimal> getUpperBound() {
return upperBound;
}
+
+ @Override
+ public String toString() {
+ return "Segment{" +
+ "identifier='" + identifier + '\'' +
+ ", lowerBound=" + lowerBound +
+ ", upperBound=" + upperBound +
+ '}';
+ }
}
Optional<ChargeRange> findChargeRange(final String productIdentifier, final ChargeDefinition chargeDefinition) {
@@ -151,41 +159,7 @@
accrueMapping = Stream.empty();
return Stream.concat(
- accrueMapping.sorted(ScheduledChargesService::proportionalityApplicationOrder),
- chargeMapping.sorted(ScheduledChargesService::proportionalityApplicationOrder));
- }
-
- static class ScheduledChargeComparator implements Comparator<ScheduledCharge>
- {
- @Override
- public int compare(ScheduledCharge o1, ScheduledCharge o2) {
- int ret = o1.getScheduledAction().when.compareTo(o2.getScheduledAction().when);
- if (ret == 0)
- ret = o1.getScheduledAction().action.compareTo(o2.getScheduledAction().action);
- if (ret == 0)
- ret = proportionalityApplicationOrder(o1.getChargeDefinition(), o2.getChargeDefinition());
- if (ret == 0)
- return o1.getChargeDefinition().getIdentifier().compareTo(o2.getChargeDefinition().getIdentifier());
- else
- return ret;
- }
- }
-
- private static int proportionalityApplicationOrder(final ChargeDefinition o1, final ChargeDefinition o2) {
- final Optional<ChargeProportionalDesignator> aProportionalToDesignator
- = ChargeProportionalDesignator.fromString(o1.getProportionalTo());
- final Optional<ChargeProportionalDesignator> bProportionalToDesignator
- = ChargeProportionalDesignator.fromString(o2.getProportionalTo());
-
- if (aProportionalToDesignator.isPresent() && bProportionalToDesignator.isPresent())
- return Integer.compare(
- aProportionalToDesignator.get().getOrderOfApplication(),
- bProportionalToDesignator.get().getOrderOfApplication());
- else if (aProportionalToDesignator.isPresent())
- return 1;
- else if (bProportionalToDesignator.isPresent())
- return -1;
- else
- return 0;
+ accrueMapping.sorted(ScheduledChargeComparator::proportionalityApplicationOrder),
+ chargeMapping.sorted(ScheduledChargeComparator::proportionalityApplicationOrder));
}
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
index c1305b8..c004eb4 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
@@ -35,6 +35,7 @@
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
+import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@@ -135,12 +136,14 @@
public List<CostComponent> getActionCostComponentsForCase(final String productIdentifier,
final String caseIdentifier,
final String actionIdentifier,
+ final LocalDateTime localDateTime,
final Set<String> forAccountDesignatorsList,
final BigDecimal forPaymentSize) {
return getPatternFactoryOrThrow(productIdentifier).getCostComponentsForAction(
productIdentifier,
caseIdentifier,
actionIdentifier,
+ localDateTime,
forAccountDesignatorsList,
forPaymentSize);
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
index bb15c70..910cb8f 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
@@ -101,18 +101,18 @@
.map(DateConverter::fromIsoString);
}
- public List<LocalDateTime> getDatesOfMostRecentTwoEntriesContainingMessage(final String accountIdentifier,
- final String message) {
+ public Optional<LocalDateTime> getDateOfMostRecentEntryContainingMessage(
+ final String accountIdentifier,
+ final String message) {
final Account account = ledgerManager.findAccount(accountIdentifier);
final LocalDateTime accountCreatedOn = DateConverter.fromIsoString(account.getCreatedOn());
final DateRange fromAccountCreationUntilNow = oneSidedDateRange(accountCreatedOn.toLocalDate());
return ledgerManager.fetchAccountEntriesStream(accountIdentifier, fromAccountCreationUntilNow.toString(), message, "DESC")
- .limit(2)
+ .findFirst()
.map(AccountEntry::getTransactionDate)
- .map(DateConverter::fromIsoString)
- .collect(Collectors.toList());
+ .map(DateConverter::fromIsoString);
}
public BigDecimal sumMatchingEntriesSinceDate(final String accountIdentifier, final LocalDate startDate, final String message)
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
index 3117f67..a2471c2 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
@@ -19,7 +19,9 @@
import io.mifos.anubis.annotation.Permittable;
import io.mifos.core.api.util.UserContextHolder;
import io.mifos.core.command.gateway.CommandGateway;
+import io.mifos.core.lang.DateConverter;
import io.mifos.core.lang.ServiceException;
+import io.mifos.core.lang.validation.constraints.ValidLocalDateTimeString;
import io.mifos.portfolio.api.v1.PermittableGroupIds;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.CasePage;
@@ -36,10 +38,13 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
@@ -192,6 +197,7 @@
List<CostComponent> getCostComponentsForAction(@PathVariable("productidentifier") final String productIdentifier,
@PathVariable("caseidentifier") final String caseIdentifier,
@PathVariable("actionidentifier") final String actionIdentifier,
+ @RequestParam(value="fordatetime", required = false, defaultValue = "") final @ValidLocalDateTimeString String forDateTimeString,
@RequestParam(value="touchingaccounts", required = false, defaultValue = "") final Set<String> forAccountDesignators,
@RequestParam(value="forpaymentsize", required = false, defaultValue = "") final BigDecimal forPaymentSize)
{
@@ -200,8 +206,15 @@
if (forPaymentSize != null && forPaymentSize.compareTo(BigDecimal.ZERO) < 0)
throw ServiceException.badRequest("forpaymentsize can''t be negative.");
+ final LocalDateTime forDateTime = StringUtils.isEmpty(forDateTimeString) ? LocalDateTime.now(Clock.systemUTC()) : DateConverter.fromIsoString(forDateTimeString);
- return caseService.getActionCostComponentsForCase(productIdentifier, caseIdentifier, actionIdentifier, forAccountDesignators, forPaymentSize);
+ return caseService.getActionCostComponentsForCase(
+ productIdentifier,
+ caseIdentifier,
+ actionIdentifier,
+ forDateTime,
+ forAccountDesignators,
+ forPaymentSize);
}
@Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CASE_MANAGEMENT)
diff --git a/service/src/main/java/io/mifos/products/spi/PatternFactory.java b/service/src/main/java/io/mifos/products/spi/PatternFactory.java
index 4b9f3c2..dd0e555 100644
--- a/service/src/main/java/io/mifos/products/spi/PatternFactory.java
+++ b/service/src/main/java/io/mifos/products/spi/PatternFactory.java
@@ -22,6 +22,7 @@
import io.mifos.portfolio.api.v1.domain.Pattern;
import java.math.BigDecimal;
+import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -41,6 +42,7 @@
String productIdentifier,
String caseIdentifier,
String actionIdentifier,
+ LocalDateTime forDateTime,
Set<String> forAccountDesignators,
BigDecimal forPaymentSize);
ProductCommandDispatcher getIndividualLendingCommandDispatcher();
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ChargeRangeTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/ChargeRangeTest.java
index a581c78..14757d7 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ChargeRangeTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ChargeRangeTest.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.individuallending.internal.service;
import org.junit.Assert;
@@ -6,6 +21,9 @@
import java.math.BigDecimal;
import java.util.Optional;
+/**
+ * @author Myrle Krantz
+ */
public class ChargeRangeTest {
@Test
public void amountIsWithinRange() throws Exception {
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
index eab6828..f2aa460 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.individuallending.internal.service;
import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
@@ -14,6 +29,9 @@
import static io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR;
import static io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR;
+/**
+ * @author Myrle Krantz
+ */
@RunWith(Parameterized.class)
public class CostComponentServiceTest {
private static class TestCase {
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java b/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
index 92002b7..13c7ff3 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
@@ -74,15 +74,15 @@
{
final List<ScheduledAction> ret = new ArrayList<>();
LocalDate begin = initial;
- for (final LocalDate paymentDate : paymentDates) {
- ret.add(scheduledRepaymentAction(begin, paymentDate));
- begin = paymentDate;
+ for (int i = 0; i < paymentDates.length; i++) {
+ ret.add(scheduledRepaymentAction(begin, paymentDates[i], (i == paymentDates.length -1)));
+ begin = paymentDates[i];
}
return ret;
}
- private static ScheduledAction scheduledRepaymentAction(final LocalDate from, final LocalDate to) {
- final Period repaymentPeriod = new Period(from, to);
+ private static ScheduledAction scheduledRepaymentAction(final LocalDate from, final LocalDate to, boolean isLast) {
+ final Period repaymentPeriod = new Period(from, to, isLast);
return new ScheduledAction(Action.ACCEPT_PAYMENT, to, repaymentPeriod, repaymentPeriod);
}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
index beeae57..4daaedc 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
@@ -102,7 +102,8 @@
REPAYMENT_ID,
TRACK_DISBURSAL_PAYMENT_ID,
TRACK_RETURN_PRINCIPAL_ID,
- DISBURSE_PAYMENT_ID
+ DISBURSE_PAYMENT_ID,
+ LATE_FEE_ID
));
private Map<ActionDatePair, List<ChargeDefinition>> chargeDefinitionsForActions = new HashMap<>();
//This is an abuse of the ChargeInstance since everywhere else it's intended to contain account identifiers and not
@@ -314,7 +315,8 @@
.collect(Collectors.toList());
//Remaining principal should correspond with the other cost components.
- final Set<BigDecimal> customerRepayments = Stream.iterate(1, x -> x + 1).limit(allPlannedPayments.size() - 1).map(x ->
+ final Set<BigDecimal> customerRepayments = Stream.iterate(1, x -> x + 1).limit(allPlannedPayments.size() - 1)
+ .map(x ->
{
final BigDecimal costComponentSum = allPlannedPayments.get(x).getCostComponents().stream()
.filter(this::includeCostComponentsInSumCheck)
@@ -330,6 +332,8 @@
Assert.assertEquals(valueOfPrincipleTrackingCostComponent, principalDifference);
Assert.assertNotEquals("Remaining principle should always be positive or zero.",
allPlannedPayments.get(x).getRemainingPrincipal().signum(), -1);
+ final boolean containsLateFee = allPlannedPayments.get(x).getCostComponents().stream().anyMatch(y -> y.getChargeIdentifier().equals(LATE_FEE_ID));
+ Assert.assertFalse("Late fee should not be included in planned payments", containsLateFee);
return costComponentSum;
}
).collect(Collectors.toSet());
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelperTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelpersTest.java
similarity index 91%
rename from service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelperTest.java
rename to service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelpersTest.java
index 9c3b265..61771b6 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelperTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelpersTest.java
@@ -35,7 +35,7 @@
* @author Myrle Krantz
*/
@RunWith(Parameterized.class)
-public class ScheduledActionHelperTest {
+public class ScheduledActionHelpersTest {
private static class TestCase
{
final String description;
@@ -355,7 +355,7 @@
private final TestCase testCase;
- public ScheduledActionHelperTest(final TestCase testCase)
+ public ScheduledActionHelpersTest(final TestCase testCase)
{
this.testCase = testCase;
}
@@ -364,8 +364,12 @@
public void getScheduledActions() throws Exception {
final List<ScheduledAction> result = ScheduledActionHelpers.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
- Assert.assertTrue("Case " + testCase.description + " should contain " + testCase.expectedResultContents,
- result.containsAll(testCase.expectedResultContents));
+ 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.when) || testCase.earliestActionDate.isEqual(x.when));
Assert.assertTrue(x.toString(), testCase.latestActionDate.isAfter(x.when) || testCase.latestActionDate.isEqual(x.when));
@@ -385,6 +389,31 @@
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.action == Action.ACCEPT_PAYMENT)
+ .forEach(expectedResultContents -> {
+ final ScheduledAction nextScheduledPayment = ScheduledActionHelpers.getNextScheduledPayment(
+ testCase.initialDisbursementDate,
+ expectedResultContents.when.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.actionPeriod);
+ Assert.assertTrue(afterAction.actionPeriod.isLastPeriod());
+ }
+
private long countActionsByType(final List<ScheduledAction> scheduledActions, final Action actionToCount) {
return scheduledActions.stream().filter(x -> x.action == actionToCount).count();
}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java
index a883ee5..a287c2f 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java
@@ -28,7 +28,7 @@
final LocalDate tomorrow = today.plusDays(1);
final LocalDate yesterday = today.minusDays(1);
final ScheduledAction testSubject = new ScheduledAction(Action.APPLY_INTEREST, today);
- Assert.assertFalse(testSubject.actionIsOnOrAfter(today));
+ Assert.assertTrue(testSubject.actionIsOnOrAfter(today));
Assert.assertFalse(testSubject.actionIsOnOrAfter(tomorrow));
Assert.assertTrue(testSubject.actionIsOnOrAfter(yesterday));
}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargeComparatorTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargeComparatorTest.java
new file mode 100644
index 0000000..2e38d67
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargeComparatorTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.individuallending.internal.service;
+
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+@RunWith(Parameterized.class)
+public class ScheduledChargeComparatorTest {
+ static class TestCase {
+ private final String description;
+ ScheduledCharge a;
+ ScheduledCharge b;
+ int expected;
+
+ TestCase(String description) {
+ this.description = description;
+ }
+
+
+ TestCase setA(ScheduledCharge a) {
+ this.a = a;
+ return this;
+ }
+
+ TestCase setB(ScheduledCharge b) {
+ this.b = b;
+ return this;
+ }
+
+ TestCase setExpected(int expected) {
+ this.expected = expected;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "TestCase{" +
+ "description='" + description + '\'' +
+ ", a=" + a +
+ ", b=" + b +
+ ", expected=" + expected +
+ '}';
+ }
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<ScheduledChargeComparatorTest.TestCase> ret = new ArrayList<>();
+
+ final ScheduledCharge trackDisbursalScheduledCharge = new ScheduledCharge(
+ SCHEDULED_DISBURSE_ACTION,
+ TRACK_DISBURSE_CHARGE_DEFINITION,
+ Optional.empty());
+
+ final ScheduledCharge disburseFeeScheduledCharge = new ScheduledCharge(
+ SCHEDULED_DISBURSE_ACTION,
+ DISBURSE_FEE_CHARGE_DEFINITION,
+ Optional.of(new ChargeRange(BigDecimal.valueOf(1000_0000, 4), Optional.empty())));
+
+ ret.add(new TestCase("disbursementFeeVstrackDisbursement")
+ .setA(trackDisbursalScheduledCharge)
+ .setB(disburseFeeScheduledCharge)
+ .setExpected(1));
+ ret.add(new TestCase("disbursementFeeVstrackDisbursement")
+ .setA(disburseFeeScheduledCharge)
+ .setB(trackDisbursalScheduledCharge)
+ .setExpected(-1));
+ ret.add(new TestCase("disbursementFeeVstrackDisbursement")
+ .setA(disburseFeeScheduledCharge)
+ .setB(disburseFeeScheduledCharge)
+ .setExpected(0));
+
+ return ret;
+ }
+
+ private final static ScheduledAction SCHEDULED_DISBURSE_ACTION = new ScheduledAction(
+ Action.DISBURSE,
+ LocalDate.of(2017, 8, 25));
+
+ private final static ChargeDefinition TRACK_DISBURSE_CHARGE_DEFINITION = new ChargeDefinition() {{
+ this.setIdentifier("track-disburse-payment");
+ this.setName("Track disburse payment");
+ this.setDescription("Track disburse payment");
+ this.setAccrueAction(null);
+ this.setChargeAction(Action.DISBURSE.name());
+ this.setAmount(BigDecimal.valueOf(100_0000, 4));
+ this.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ this.setProportionalTo("{principaladjustment}");
+ this.setFromAccountDesignator("pending-disbursal");
+ this.setAccrualAccountDesignator(null);
+ this.setToAccountDesignator("customer-loan");
+ this.setForCycleSizeUnit(null);
+ this.setReadOnly(true);
+ this.setForSegmentSet(null);
+ this.setFromSegment(null);
+ this.setToSegment(null);
+ }};
+
+ private final static ChargeDefinition DISBURSE_FEE_CHARGE_DEFINITION = new ChargeDefinition() {{
+ this.setIdentifier("disbursement-fee2");
+ this.setName("disbursement-fee2");
+ this.setDescription("Disbursement fee");
+ this.setAccrueAction(null);
+ this.setChargeAction(Action.DISBURSE.name());
+ this.setAmount(BigDecimal.valueOf(1_0000, 4));
+ this.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ this.setProportionalTo("{principaladjustment}");
+ this.setFromAccountDesignator("'entry'");
+ this.setAccrualAccountDesignator(null);
+ this.setToAccountDesignator("disbursement-fee-income");
+ this.setForCycleSizeUnit(null);
+ this.setReadOnly(false);
+ this.setForSegmentSet("disbursement_ranges");
+ this.setFromSegment("larger");
+ this.setToSegment("larger");
+ }};
+
+ private final ScheduledChargeComparatorTest.TestCase testCase;
+
+ public ScheduledChargeComparatorTest(final ScheduledChargeComparatorTest.TestCase testCase) {
+ this.testCase = testCase;
+ }
+
+ @Test
+ public void compare() {
+ Assert.assertEquals(testCase.expected == 0, ScheduledChargeComparator.compareScheduledCharges(testCase.a, testCase.b) == 0);
+ Assert.assertEquals(testCase.expected > 0, ScheduledChargeComparator.compareScheduledCharges(testCase.a, testCase.b) > 0);
+ Assert.assertEquals(testCase.expected < 0, ScheduledChargeComparator.compareScheduledCharges(testCase.a, testCase.b) < 0);
+ }
+
+}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargesServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargesServiceTest.java
index 032e223..57cee42 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargesServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargesServiceTest.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed 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 io.mifos.individuallending.internal.service;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
@@ -13,6 +28,9 @@
import java.math.BigDecimal;
import java.util.*;
+/**
+ * @author Myrle Krantz
+ */
@RunWith(Parameterized.class)
public class ScheduledChargesServiceTest {