Using range associations for charges to determine which charges to apply when.
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 f533a12..dc3cdfe 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -52,7 +52,7 @@
product.setName("Agricultural Loan");
product.setDescription("Loan for seeds or agricultural equipment");
product.setTermRange(new TermRange(ChronoUnit.MONTHS, 12));
- product.setBalanceRange(new BalanceRange(fixScale(BigDecimal.ZERO), fixScale(new BigDecimal(10000))));
+ product.setBalanceRange(new BalanceRange(fixScale(BigDecimal.ZERO), fixScale(new BigDecimal(10_000))));
product.setInterestRange(new InterestRange(BigDecimal.valueOf(3_00, 2), BigDecimal.valueOf(12_00, 2)));
product.setInterestBasis(InterestBasis.CURRENT_BALANCE);
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 a49d578..c4a9e5f 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -54,10 +54,10 @@
* @author Myrle Krantz
*/
public class TestAccountingInteractionInLoanWorkflow extends AbstractPortfolioTest {
- private static final BigDecimal PROCESSING_FEE_AMOUNT = BigDecimal.valueOf(10_0000, MINOR_CURRENCY_UNIT_DIGITS);
- private static final BigDecimal LOAN_ORIGINATION_FEE_AMOUNT = BigDecimal.valueOf(100_0000, MINOR_CURRENCY_UNIT_DIGITS);
- private static final BigDecimal DISBURSEMENT_FEE_LOWER_RANGE_AMOUNT = BigDecimal.valueOf(10_0000, MINOR_CURRENCY_UNIT_DIGITS);
- private static final BigDecimal DISBURSEMENT_FEE_UPPER_RANGE_AMOUNT = BigDecimal.valueOf(1_0000, MINOR_CURRENCY_UNIT_DIGITS);
+ private static final BigDecimal PROCESSING_FEE_AMOUNT = BigDecimal.valueOf(100_00, MINOR_CURRENCY_UNIT_DIGITS);
+ private static final BigDecimal LOAN_ORIGINATION_FEE_AMOUNT = BigDecimal.valueOf(100_00, MINOR_CURRENCY_UNIT_DIGITS);
+ private static final BigDecimal DISBURSEMENT_FEE_LOWER_RANGE_AMOUNT = BigDecimal.valueOf(10_00, MINOR_CURRENCY_UNIT_DIGITS);
+ private static final BigDecimal DISBURSEMENT_FEE_UPPER_RANGE_AMOUNT = BigDecimal.valueOf(1_00, MINOR_CURRENCY_UNIT_DIGITS);
private static final String DISBURSEMENT_RANGES = "disbursement_ranges";
private static final String DISBURSEMENT_LOWER_RANGE = "smaller";
private static final String DISBURSEMENT_UPPER_RANGE = "larger";
@@ -95,7 +95,26 @@
step2CreateCase();
step3OpenCase();
step4ApproveCase();
- step5Disburse(BigDecimal.valueOf(2000L).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
+ 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);
+ step8Close();
+ }
+
+ @Test
+ public void workflowWithTwoUnequalDisbursals() throws InterruptedException {
+ step1CreateProduct();
+ step2CreateCase();
+ step3OpenCase();
+ step4ApproveCase();
+ step5Disburse(
+ BigDecimal.valueOf(500_00, MINOR_CURRENCY_UNIT_DIGITS),
+ ChargeIdentifiers.DISBURSEMENT_FEE_ID, BigDecimal.valueOf(10_00, MINOR_CURRENCY_UNIT_DIGITS));
+ 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);
step8Close();
@@ -107,7 +126,9 @@
step2CreateCase();
step3OpenCase();
step4ApproveCase();
- step5Disburse(BigDecimal.valueOf(2000L).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
+ step5Disburse(
+ BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
+ UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
step6CalculateInterestAccrual();
final BigDecimal repayment1 = expectedCurrentBalance.divide(BigDecimal.valueOf(2), BigDecimal.ROUND_HALF_EVEN);
step7PaybackPartialAmount(repayment1.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
@@ -122,7 +143,8 @@
step3OpenCase();
step4ApproveCase();
try {
- step5Disburse(BigDecimal.valueOf(-2).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
+ step5Disburse(BigDecimal.valueOf(-2).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN),
+ UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
Assert.fail("Expected an IllegalArgumentException.");
}
catch (IllegalArgumentException ignored) { }
@@ -147,7 +169,7 @@
= portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.DISBURSEMENT_FEE_ID);
lowerRangeDisbursementFeeChargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
lowerRangeDisbursementFeeChargeDefinition.setAmount(DISBURSEMENT_FEE_LOWER_RANGE_AMOUNT);
- lowerRangeDisbursementFeeChargeDefinition.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.name());
+ lowerRangeDisbursementFeeChargeDefinition.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
lowerRangeDisbursementFeeChargeDefinition.setForSegmentSet(DISBURSEMENT_RANGES);
lowerRangeDisbursementFeeChargeDefinition.setFromSegment(DISBURSEMENT_LOWER_RANGE);
lowerRangeDisbursementFeeChargeDefinition.setToSegment(DISBURSEMENT_LOWER_RANGE);
@@ -167,7 +189,7 @@
upperRangeDisbursementFeeChargeDefinition.setChargeAction(lowerRangeDisbursementFeeChargeDefinition.getChargeAction());
upperRangeDisbursementFeeChargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
upperRangeDisbursementFeeChargeDefinition.setAmount(DISBURSEMENT_FEE_UPPER_RANGE_AMOUNT);
- upperRangeDisbursementFeeChargeDefinition.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.name());
+ upperRangeDisbursementFeeChargeDefinition.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
upperRangeDisbursementFeeChargeDefinition.setForSegmentSet(DISBURSEMENT_RANGES);
upperRangeDisbursementFeeChargeDefinition.setFromSegment(DISBURSEMENT_UPPER_RANGE);
upperRangeDisbursementFeeChargeDefinition.setToSegment(DISBURSEMENT_UPPER_RANGE);
@@ -278,15 +300,17 @@
}
//Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
- private void step5Disburse(final BigDecimal amount) throws InterruptedException {
+ private void step5Disburse(
+ final BigDecimal amount,
+ final String whichDisbursementFee,
+ final BigDecimal disbursementFeeAmount) throws InterruptedException {
logger.info("step5Disburse");
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE,
Collections.singleton(AccountDesignators.ENTRY),
- amount, new CostComponent(ChargeIdentifiers.DISBURSEMENT_FEE_ID, DISBURSEMENT_FEE_LOWER_RANGE_AMOUNT),
- new CostComponent(UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN)),
+ amount, new CostComponent(whichDisbursementFee, disbursementFeeAmount),
new CostComponent(ChargeIdentifiers.DISBURSE_PAYMENT_ID, amount));
checkStateTransfer(
product.getIdentifier(),
@@ -303,12 +327,12 @@
final Set<Debtor> debtors = new HashSet<>();
debtors.add(new Debtor(pendingDisbursalAccountIdentifier, amount.toPlainString()));
debtors.add(new Debtor(AccountingFixture.LOANS_PAYABLE_ACCOUNT_IDENTIFIER, amount.toPlainString()));
- debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, DISBURSEMENT_FEE_LOWER_RANGE_AMOUNT.toPlainString()));
+ debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, disbursementFeeAmount.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
creditors.add(new Creditor(customerLoanAccountIdentifier, amount.toPlainString()));
creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, amount.toPlainString()));
- creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, DISBURSEMENT_FEE_LOWER_RANGE_AMOUNT.toPlainString()));
+ creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, disbursementFeeAmount.toPlainString()));
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE);
expectedCurrentBalance = expectedCurrentBalance.add(amount);
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ChargeRange.java b/service/src/main/java/io/mifos/individuallending/internal/service/ChargeRange.java
new file mode 100644
index 0000000..cd293d5
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ChargeRange.java
@@ -0,0 +1,64 @@
+/*
+ * 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 java.math.BigDecimal;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+class ChargeRange {
+ final private BigDecimal from;
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ final private Optional<BigDecimal> to;
+
+ ChargeRange(
+ final BigDecimal from,
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<BigDecimal> to) {
+ this.from = from;
+ this.to = to;
+ }
+
+ boolean amountIsWithinRange(BigDecimal amountProportionalTo) {
+ return to.map(bigDecimal -> from.compareTo(amountProportionalTo) <= 0 &&
+ bigDecimal.compareTo(amountProportionalTo) > 0)
+ .orElseGet(() -> from.compareTo(amountProportionalTo) <= 0);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ChargeRange that = (ChargeRange) o;
+ return Objects.equals(from, that.from) &&
+ Objects.equals(to, that.to);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(from, to);
+ }
+
+ @Override
+ public String toString() {
+ return "ChargeRange{" +
+ "from=" + from +
+ ", to=" + to +
+ '}';
+ }
+}
\ No newline at end of file
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 318db3b..c3ba820 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
@@ -47,14 +47,14 @@
private static final int EXTRA_PRECISION = 4;
private static final int RUNNING_CALCULATION_PRECISION = 8;
- private final IndividualLoanService individualLoanService;
+ private final ScheduledChargesService scheduledChargesService;
private final AccountingAdapter accountingAdapter;
@Autowired
public CostComponentService(
- final IndividualLoanService individualLoanService,
- final AccountingAdapter accountingAdapter) {
- this.individualLoanService = individualLoanService;
+ final ScheduledChargesService scheduledChargesService,
+ final AccountingAdapter accountingAdapter) {
+ this.scheduledChargesService = scheduledChargesService;
this.accountingAdapter = accountingAdapter;
}
@@ -93,7 +93,7 @@
final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.OPEN, today()));
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier, scheduledActions);
return getCostComponentsForScheduledCharges(
@@ -112,7 +112,7 @@
final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.DENY, today()));
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier, scheduledActions);
return getCostComponentsForScheduledCharges(
@@ -132,7 +132,7 @@
final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.APPROVE, today()));
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier, scheduledActions);
return getCostComponentsForScheduledCharges(
@@ -173,7 +173,7 @@
else
disbursalSize = requestedDisbursalSize.negate();
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier, scheduledActions);
@@ -219,7 +219,7 @@
final LocalDate today = today();
final ScheduledAction interestAction = new ScheduledAction(Action.APPLY_INTEREST, today, new Period(1, today));
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier,
Collections.singletonList(interestAction));
@@ -271,7 +271,7 @@
final List<ScheduledAction> hypotheticalScheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(
today(),
caseParameters);
- final List<ScheduledCharge> hypotheticalScheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> hypotheticalScheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier,
hypotheticalScheduledActions);
loanPaymentSize = getLoanPaymentSize(
@@ -281,7 +281,7 @@
hypotheticalScheduledCharges);
}
- final List<ScheduledCharge> scheduledChargesForThisAction = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledChargesForThisAction = scheduledChargesService.getScheduledCharges(
productIdentifier,
Collections.singletonList(scheduledAction));
@@ -307,12 +307,12 @@
true);
}
- public static boolean isAccruedChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
+ private static boolean isAccruedChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
return chargeDefinition.getAccrueAction() != null &&
chargeDefinition.getChargeAction().equals(action.name());
}
- public static boolean isAccrualChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
+ private static boolean isAccrualChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
return chargeDefinition.getAccrueAction() != null &&
chargeDefinition.getAccrueAction().equals(action.name());
}
@@ -367,7 +367,7 @@
final LocalDate today = today();
final ScheduledAction closeAction = new ScheduledAction(Action.CLOSE, today, new Period(1, today));
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier,
Collections.singletonList(closeAction));
@@ -440,6 +440,9 @@
entryAccountAdjustment,
balanceAdjustments);
//TODO: getAmountProportionalTo is programmed under the assumption of non-accrual accounting.
+ if (scheduledCharge.getChargeRange().map(x ->
+ !x.amountIsWithinRange(amountProportionalTo)).orElse(false))
+ continue;
final CostComponent costComponent = costComponentMap
.computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
@@ -468,27 +471,37 @@
final BigDecimal runningBalance,
final BigDecimal loanPaymentSize,
final Map<String, BigDecimal> balanceAdjustments) {
- final Optional<ChargeProportionalDesignator> optionalChargeProportionalTo = proportionalToDesignator(scheduledCharge);
- return optionalChargeProportionalTo.map(chargeProportionalTo -> {
- switch (chargeProportionalTo) {
- case NOT_PROPORTIONAL:
- return BigDecimal.ZERO;
- case MAXIMUM_BALANCE_DESIGNATOR:
- return maximumBalance;
- case RUNNING_BALANCE_DESIGNATOR:
- return runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
- case REPAYMENT_DESIGNATOR:
- return loanPaymentSize;
- case PRINCIPAL_ADJUSTMENT_DESIGNATOR: {
- final BigDecimal newRunningBalance
- = runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
- final BigDecimal newLoanPaymentSize = loanPaymentSize.min(newRunningBalance);
- return newLoanPaymentSize.add(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO)).abs();
- }
- default:
- return BigDecimal.ZERO;
+ final Optional<ChargeProportionalDesignator> optionalChargeProportionalTo
+ = ChargeProportionalDesignator.fromString(scheduledCharge.getChargeDefinition().getProportionalTo());
+ return optionalChargeProportionalTo.map(chargeProportionalTo ->
+ getAmountProportionalTo(chargeProportionalTo, maximumBalance, runningBalance, loanPaymentSize, balanceAdjustments))
+ .orElse(BigDecimal.ZERO);
+ }
+
+ static BigDecimal getAmountProportionalTo(
+ final ChargeProportionalDesignator chargeProportionalTo,
+ final BigDecimal maximumBalance,
+ final BigDecimal runningBalance,
+ final BigDecimal loanPaymentSize,
+ final Map<String, BigDecimal> balanceAdjustments) {
+ switch (chargeProportionalTo) {
+ case NOT_PROPORTIONAL:
+ return BigDecimal.ZERO;
+ case MAXIMUM_BALANCE_DESIGNATOR:
+ return maximumBalance;
+ case RUNNING_BALANCE_DESIGNATOR:
+ return runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
+ case REPAYMENT_DESIGNATOR:
+ return loanPaymentSize;
+ case PRINCIPAL_ADJUSTMENT_DESIGNATOR: {
+ final BigDecimal newRunningBalance
+ = runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
+ final BigDecimal newLoanPaymentSize = loanPaymentSize.min(newRunningBalance);
+ return newLoanPaymentSize.add(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO)).abs();
}
- }).orElse(BigDecimal.ZERO);
+ default:
+ return BigDecimal.ZERO;
+ }
//TODO: correctly implement charges which are proportional to other charges.
}
@@ -499,14 +512,6 @@
return ret;
}
- private static Optional<ChargeProportionalDesignator> proportionalToDesignator(final ScheduledCharge scheduledCharge) {
- if (!scheduledCharge.getChargeDefinition().getChargeMethod().equals(ChargeDefinition.ChargeMethod.PROPORTIONAL) &&
- !scheduledCharge.getChargeDefinition().getChargeMethod().equals(ChargeDefinition.ChargeMethod.INTEREST))
- return Optional.of(ChargeProportionalDesignator.NOT_PROPORTIONAL);
-
- return ChargeProportionalDesignator.fromString(scheduledCharge.getChargeDefinition().getProportionalTo());
- }
-
private static Function<BigDecimal, BigDecimal> howToApplyScheduledChargeToAmount(
final ScheduledCharge scheduledCharge, final BigDecimal interest)
{
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 6320deb..e836004 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
@@ -18,9 +18,6 @@
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.product.ChargeProportionalDesignator;
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
-import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -30,18 +27,17 @@
import java.util.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* @author Myrle Krantz
*/
@Service
public class IndividualLoanService {
- private final ChargeDefinitionService chargeDefinitionService;
+ private final ScheduledChargesService scheduledChargesService;
@Autowired
- public IndividualLoanService(final ChargeDefinitionService chargeDefinitionService) {
- this.chargeDefinitionService = chargeDefinitionService;
+ public IndividualLoanService(final ScheduledChargesService scheduledChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
}
public PlannedPaymentPage getPlannedPaymentsPage(
@@ -53,7 +49,7 @@
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(initialDisbursalDate, dataContextOfAction.getCaseParameters());
- final List<ScheduledCharge> scheduledCharges = getScheduledCharges(dataContextOfAction.getProduct().getIdentifier(), scheduledActions);
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(dataContextOfAction.getProduct().getIdentifier(), scheduledActions);
final BigDecimal loanPaymentSize = CostComponentService.getLoanPaymentSize(
dataContextOfAction.getCaseParameters().getMaximumBalance(),
@@ -98,21 +94,6 @@
return new ChargeName(scheduledCharge.getChargeDefinition().getIdentifier(), scheduledCharge.getChargeDefinition().getName());
}
- List<ScheduledCharge> getScheduledCharges(
- final String productIdentifier,
- final @Nonnull List<ScheduledAction> scheduledActions) {
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction
- = chargeDefinitionService.getChargeDefinitionsMappedByChargeAction(productIdentifier);
-
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction
- = chargeDefinitionService.getChargeDefinitionsMappedByAccrueAction(productIdentifier);
-
- return getScheduledCharges(
- scheduledActions,
- chargeDefinitionsMappedByChargeAction,
- chargeDefinitionsMappedByAccrueAction);
- }
-
static private List<PlannedPayment> getPlannedPaymentsElements(
final BigDecimal initialBalance,
final int minorCurrencyUnitDigits,
@@ -124,7 +105,7 @@
.collect(Collectors.groupingBy(IndividualLoanService::getPeriodFromScheduledCharge,
Collectors.mapping(x -> x,
Collector.of(
- () -> new TreeSet<>(new ScheduledChargeComparator()),
+ () -> new TreeSet<>(new ScheduledChargesService.ScheduledChargeComparator()),
SortedSet::add,
(left, right) -> { left.addAll(right); return left; }))));
@@ -179,72 +160,4 @@
else
return scheduledAction.repaymentPeriod;
}
-
- static List<ScheduledCharge> getScheduledCharges(final List<ScheduledAction> scheduledActions,
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction) {
- return scheduledActions.stream()
- .flatMap(scheduledAction ->
- getChargeDefinitionStream(
- chargeDefinitionsMappedByChargeAction,
- chargeDefinitionsMappedByAccrueAction,
- scheduledAction)
- .map(chargeDefinition -> new ScheduledCharge(scheduledAction, chargeDefinition)))
- .collect(Collectors.toList());
- }
-
- private static Stream<ChargeDefinition> getChargeDefinitionStream(
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
- final ScheduledAction scheduledAction) {
- final List<ChargeDefinition> chargeMappingList = chargeDefinitionsMappedByChargeAction
- .get(scheduledAction.action.name());
- Stream<ChargeDefinition> chargeMapping = chargeMappingList == null ? Stream.empty() : chargeMappingList.stream();
- if (chargeMapping == null)
- chargeMapping = Stream.empty();
-
- final List<ChargeDefinition> accrueMappingList = chargeDefinitionsMappedByAccrueAction
- .get(scheduledAction.action.name());
- Stream<ChargeDefinition> accrueMapping = accrueMappingList == null ? Stream.empty() : accrueMappingList.stream();
- if (accrueMapping == null)
- accrueMapping = Stream.empty();
-
- return Stream.concat(
- accrueMapping.sorted(IndividualLoanService::proportionalityApplicationOrder),
- chargeMapping.sorted(IndividualLoanService::proportionalityApplicationOrder));
- }
-
- private 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;
- }
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java
index 60fb56c..7b98317 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java
@@ -18,6 +18,7 @@
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import javax.annotation.Nonnull;
+import java.util.Optional;
/**
* @author Myrle Krantz
@@ -25,10 +26,16 @@
public class ScheduledCharge {
private final ScheduledAction scheduledAction;
private final ChargeDefinition chargeDefinition;
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ private final Optional<ChargeRange> chargeRange;
- ScheduledCharge(@Nonnull final ScheduledAction scheduledAction, @Nonnull final ChargeDefinition chargeDefinition) {
+ ScheduledCharge(
+ @Nonnull final ScheduledAction scheduledAction,
+ @Nonnull final ChargeDefinition chargeDefinition,
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Nonnull final Optional<ChargeRange> chargeRange) {
this.scheduledAction = scheduledAction;
this.chargeDefinition = chargeDefinition;
+ this.chargeRange = chargeRange;
}
ScheduledAction getScheduledAction() {
@@ -39,11 +46,16 @@
return chargeDefinition;
}
+ Optional<ChargeRange> getChargeRange() {
+ return chargeRange;
+ }
+
@Override
public String toString() {
return "ScheduledCharge{" +
- "scheduledAction=" + scheduledAction +
- ", chargeDefinition=" + chargeDefinition +
- '}';
+ "scheduledAction=" + scheduledAction +
+ ", chargeDefinition=" + chargeDefinition +
+ ", chargeRange=" + chargeRange +
+ '}';
}
}
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
new file mode 100644
index 0000000..fcbfc6c
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargesService.java
@@ -0,0 +1,191 @@
+/*
+ * 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 io.mifos.portfolio.service.internal.repository.BalanceSegmentEntity;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
+import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Nonnull;
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class ScheduledChargesService {
+ private final ChargeDefinitionService chargeDefinitionService;
+ private final BalanceSegmentRepository balanceSegmentRepository;
+
+ @Autowired
+ public ScheduledChargesService(
+ final ChargeDefinitionService chargeDefinitionService,
+ final BalanceSegmentRepository balanceSegmentRepository) {
+ this.chargeDefinitionService = chargeDefinitionService;
+ this.balanceSegmentRepository = balanceSegmentRepository;
+ }
+
+ List<ScheduledCharge> getScheduledCharges(
+ final String productIdentifier,
+ final @Nonnull List<ScheduledAction> scheduledActions) {
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction
+ = chargeDefinitionService.getChargeDefinitionsMappedByChargeAction(productIdentifier);
+
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction
+ = chargeDefinitionService.getChargeDefinitionsMappedByAccrueAction(productIdentifier);
+
+ return getScheduledCharges(
+ productIdentifier,
+ scheduledActions,
+ chargeDefinitionsMappedByChargeAction,
+ chargeDefinitionsMappedByAccrueAction);
+ }
+
+ private List<ScheduledCharge> getScheduledCharges(
+ final String productIdentifier,
+ final List<ScheduledAction> scheduledActions,
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction) {
+ return scheduledActions.stream()
+ .flatMap(scheduledAction ->
+ getChargeDefinitionStream(
+ chargeDefinitionsMappedByChargeAction,
+ chargeDefinitionsMappedByAccrueAction,
+ scheduledAction)
+ .map(chargeDefinition -> new ScheduledCharge(
+ scheduledAction,
+ chargeDefinition,
+ findChargeRange(productIdentifier, chargeDefinition))))
+ .collect(Collectors.toList());
+ }
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ private static class Segment {
+ final String identifier;
+ final BigDecimal lowerBound;
+ final Optional<BigDecimal> upperBound;
+
+ private Segment(final String segmentIdentifier,
+ final BigDecimal lowerBound,
+ final Optional<BigDecimal> upperBound) {
+ this.identifier = segmentIdentifier;
+ this.lowerBound = lowerBound;
+ this.upperBound = upperBound;
+ }
+
+ BigDecimal getLowerBound() {
+ return lowerBound;
+ }
+
+ Optional<BigDecimal> getUpperBound() {
+ return upperBound;
+ }
+ }
+
+ Optional<ChargeRange> findChargeRange(final String productIdentifier, final ChargeDefinition chargeDefinition) {
+ if ((chargeDefinition.getForSegmentSet() == null) ||
+ (chargeDefinition.getFromSegment() == null) ||
+ (chargeDefinition.getToSegment() == null))
+ return Optional.empty();
+
+ final List<BalanceSegmentEntity> segmentSet = balanceSegmentRepository.findByProductIdentifierAndSegmentSetIdentifier(productIdentifier, chargeDefinition.getForSegmentSet())
+ .sorted(Comparator.comparing(BalanceSegmentEntity::getLowerBound))
+ .collect(Collectors.toList());
+
+ final Map<String, Segment> segments = Stream.iterate(0, i -> i + 1).limit(segmentSet.size())
+ .map(i -> new Segment(
+ segmentSet.get(i).getSegmentIdentifier(),
+ segmentSet.get(i).getLowerBound(),
+ Optional.ofNullable(i + 1 < segmentSet.size() ?
+ segmentSet.get(i + 1).getLowerBound() :
+ null)
+ ))
+ .collect(Collectors.toMap(x -> x.identifier, x -> x));
+
+
+ final Optional<Segment> fromSegment = Optional.ofNullable(segments.get(chargeDefinition.getFromSegment()));
+ final Optional<Segment> toSegment = Optional.ofNullable(segments.get(chargeDefinition.getToSegment()));
+ if (!fromSegment.isPresent() || !toSegment.isPresent())
+ return Optional.empty();
+
+ return Optional.of(new ChargeRange(fromSegment.get().getLowerBound(), toSegment.get().getUpperBound()));
+ }
+
+ private static Stream<ChargeDefinition> getChargeDefinitionStream(
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
+ final ScheduledAction scheduledAction) {
+ final List<ChargeDefinition> chargeMappingList = chargeDefinitionsMappedByChargeAction
+ .get(scheduledAction.action.name());
+ Stream<ChargeDefinition> chargeMapping = chargeMappingList == null ? Stream.empty() : chargeMappingList.stream();
+ if (chargeMapping == null)
+ chargeMapping = Stream.empty();
+
+ final List<ChargeDefinition> accrueMappingList = chargeDefinitionsMappedByAccrueAction
+ .get(scheduledAction.action.name());
+ Stream<ChargeDefinition> accrueMapping = accrueMappingList == null ? Stream.empty() : accrueMappingList.stream();
+ if (accrueMapping == null)
+ 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;
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/BalanceSegmentEntity.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/BalanceSegmentEntity.java
index b2d7e8b..a395c02 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/repository/BalanceSegmentEntity.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/BalanceSegmentEntity.java
@@ -46,6 +46,13 @@
public BalanceSegmentEntity() {
}
+ public BalanceSegmentEntity(ProductEntity product, String segmentSetIdentifier, String segmentIdentifier, BigDecimal lowerBound) {
+ this.product = product;
+ this.segmentSetIdentifier = segmentSetIdentifier;
+ this.segmentIdentifier = segmentIdentifier;
+ this.lowerBound = lowerBound;
+ }
+
public Long getId() {
return id;
}
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
new file mode 100644
index 0000000..a581c78
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ChargeRangeTest.java
@@ -0,0 +1,25 @@
+package io.mifos.individuallending.internal.service;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+
+public class ChargeRangeTest {
+ @Test
+ public void amountIsWithinRange() throws Exception {
+ final ChargeRange testSubject1 = new ChargeRange(BigDecimal.TEN, Optional.empty());
+ Assert.assertFalse(testSubject1.amountIsWithinRange(BigDecimal.ZERO));
+ Assert.assertFalse(testSubject1.amountIsWithinRange(BigDecimal.ONE));
+ Assert.assertTrue(testSubject1.amountIsWithinRange(BigDecimal.TEN));
+ Assert.assertTrue(testSubject1.amountIsWithinRange(BigDecimal.TEN.add(BigDecimal.ONE)));
+
+ final ChargeRange testSubject2 = new ChargeRange(BigDecimal.ZERO, Optional.of(BigDecimal.TEN));
+ Assert.assertTrue(testSubject2.amountIsWithinRange(BigDecimal.ZERO));
+ Assert.assertTrue(testSubject2.amountIsWithinRange(BigDecimal.ONE));
+ Assert.assertFalse(testSubject2.amountIsWithinRange(BigDecimal.TEN));
+ Assert.assertFalse(testSubject2.amountIsWithinRange(BigDecimal.TEN.add(BigDecimal.ONE)));
+ }
+
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..eab6828
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
@@ -0,0 +1,91 @@
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+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;
+
+@RunWith(Parameterized.class)
+public class CostComponentServiceTest {
+ private static class TestCase {
+ final String description;
+ ChargeProportionalDesignator chargeProportionalDesignator = ChargeProportionalDesignator.NOT_PROPORTIONAL;
+ BigDecimal maximumBalance = BigDecimal.ZERO;
+ BigDecimal runningBalance = BigDecimal.ZERO;
+ BigDecimal loanPaymentSize = BigDecimal.ZERO;
+ BigDecimal expectedAmount = BigDecimal.ZERO;
+
+ private TestCase(String description) {
+ this.description = description;
+ }
+
+ TestCase chargeProportionalDesignator(ChargeProportionalDesignator chargeProportionalDesignator) {
+ this.chargeProportionalDesignator = chargeProportionalDesignator;
+ return this;
+ }
+
+ TestCase maximumBalance(BigDecimal maximumBalance) {
+ this.maximumBalance = maximumBalance;
+ return this;
+ }
+
+ TestCase runningBalance(BigDecimal runningBalance) {
+ this.runningBalance = runningBalance;
+ return this;
+ }
+
+ TestCase loanPaymentSize(BigDecimal loanPaymentSize) {
+ this.loanPaymentSize = loanPaymentSize;
+ return this;
+ }
+
+ TestCase expectedAmount(BigDecimal expectedAmount) {
+ this.expectedAmount = expectedAmount;
+ return this;
+ }
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<CostComponentServiceTest.TestCase> ret = new ArrayList<>();
+ ret.add(new TestCase("simple"));
+ ret.add(new TestCase("distribution fee")
+ .chargeProportionalDesignator(PRINCIPAL_ADJUSTMENT_DESIGNATOR)
+ .maximumBalance(BigDecimal.valueOf(2000))
+ .loanPaymentSize(BigDecimal.valueOf(-2000))
+ .expectedAmount(BigDecimal.valueOf(2000)));
+ ret.add(new TestCase("origination fee")
+ .chargeProportionalDesignator(RUNNING_BALANCE_DESIGNATOR)
+ .runningBalance(BigDecimal.valueOf(5000))
+ .expectedAmount(BigDecimal.valueOf(5000)));
+ return ret;
+ }
+
+ private final CostComponentServiceTest.TestCase testCase;
+
+ public CostComponentServiceTest(final CostComponentServiceTest.TestCase testCase) {
+ this.testCase = testCase;
+ }
+
+ @Test
+ public void getAmountProportionalTo() {
+ final BigDecimal amount = CostComponentService.getAmountProportionalTo(
+ testCase.chargeProportionalDesignator,
+ testCase.maximumBalance,
+ testCase.runningBalance,
+ testCase.loanPaymentSize,
+ Collections.emptyMap());
+
+ Assert.assertEquals(testCase.expectedAmount, amount);
+ }
+
+}
\ No newline at end of file
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 7f73106..92002b7 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
@@ -28,6 +28,7 @@
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import java.util.Random;
import static java.math.BigDecimal.ROUND_HALF_EVEN;
@@ -107,7 +108,7 @@
chargeDefinition.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN);
chargeDefinition.setAccrualAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
chargeDefinition.setToAccountDesignator(AccountDesignators.INTEREST_INCOME);
- return new ScheduledCharge(scheduledAction, chargeDefinition);
+ return new ScheduledCharge(scheduledAction, chargeDefinition, Optional.empty());
}
static Period getPeriod(final LocalDate initialDate, final int periodBeginDelta, final int periodLength) {
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 1bb13b1..fc31ae7 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
@@ -24,6 +24,7 @@
import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.portfolio.api.v1.domain.*;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
import io.mifos.portfolio.service.internal.repository.CaseEntity;
import io.mifos.portfolio.service.internal.repository.ProductEntity;
import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
@@ -31,6 +32,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import org.mockito.Matchers;
import org.mockito.Mockito;
import java.math.BigDecimal;
@@ -47,6 +49,7 @@
*/
@RunWith(Parameterized.class)
public class IndividualLoanServiceTest {
+
private static class ActionDatePair {
final Action action;
final LocalDate localDate;
@@ -171,6 +174,7 @@
private final TestCase testCase;
private final IndividualLoanService testSubject;
+ private final ScheduledChargesService scheduledChargesService;
private final Map<String, List<ChargeDefinition>> chargeDefinitionsByChargeAction;
private final Map<String, List<ChargeDefinition>> chargeDefinitionsByAccrueAction;
@@ -282,7 +286,12 @@
Mockito.doReturn(chargeDefinitionsByChargeAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByChargeAction(testCase.productIdentifier);
Mockito.doReturn(chargeDefinitionsByAccrueAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByAccrueAction(testCase.productIdentifier);
- testSubject = new IndividualLoanService(chargeDefinitionServiceMock);
+ final BalanceSegmentRepository balanceSegmentRepositoryMock = Mockito.mock(BalanceSegmentRepository.class);
+ Mockito.doReturn(Stream.empty()).when(balanceSegmentRepositoryMock).findByProductIdentifierAndSegmentSetIdentifier(Matchers.anyString(), Matchers.anyString());
+
+ scheduledChargesService = new ScheduledChargesService(chargeDefinitionServiceMock, balanceSegmentRepositoryMock);
+
+ testSubject = new IndividualLoanService(scheduledChargesService);
}
@Test
@@ -376,7 +385,7 @@
@Test
public void getScheduledCharges() {
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
- final List<ScheduledCharge> scheduledCharges = testSubject.getScheduledCharges(testCase.productIdentifier,
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(testCase.productIdentifier,
scheduledActions);
final List<LocalDate> interestCalculationDates = scheduledCharges.stream()
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
new file mode 100644
index 0000000..032e223
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargesServiceTest.java
@@ -0,0 +1,111 @@
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentEntity;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
+import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.Mockito;
+
+import java.math.BigDecimal;
+import java.util.*;
+
+@RunWith(Parameterized.class)
+public class ScheduledChargesServiceTest {
+
+ private static final String PRODUCT_IDENTIFIER = "a";
+ private static final String SEGMENT_SET_IDENTIFIER = "b";
+
+ static class TestCase {
+ final String description;
+ List<BalanceSegmentEntity> balanceSegmentEntities = Collections.emptyList();
+ String fromSegment = null;
+ String toSegment = null;
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ Optional<ChargeRange> expectedResult = Optional.empty();
+
+ private TestCase(String description) {
+ this.description = description;
+ }
+
+ TestCase balanceSegmentEntities(List<BalanceSegmentEntity> balanceSegmentEntities) {
+ this.balanceSegmentEntities = balanceSegmentEntities;
+ return this;
+ }
+
+ TestCase fromSegment(String fromSegment) {
+ this.fromSegment = fromSegment;
+ return this;
+ }
+
+ TestCase toSegment(String toSegment) {
+ this.toSegment = toSegment;
+ return this;
+ }
+
+ TestCase expectedResult(@SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional<ChargeRange> expectedResult) {
+ this.expectedResult = expectedResult;
+ return this;
+ }
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<ScheduledChargesServiceTest.TestCase> ret = new ArrayList<>();
+ ret.add(new ScheduledChargesServiceTest.TestCase("simple"));
+ ret.add(new TestCase("two segments, one referenced")
+ .balanceSegmentEntities(Arrays.asList(
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "first", BigDecimal.ZERO),
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "second", BigDecimal.TEN)))
+ .fromSegment("first")
+ .toSegment("first")
+ .expectedResult(Optional.of(new ChargeRange(BigDecimal.ZERO, Optional.of(BigDecimal.TEN))))
+ );
+ ret.add(new TestCase("two segments, both referenced")
+ .balanceSegmentEntities(Arrays.asList(
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "lower", BigDecimal.ZERO),
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "higher", BigDecimal.TEN)))
+ .fromSegment("lower")
+ .toSegment("higher")
+ .expectedResult(Optional.of(new ChargeRange(BigDecimal.ZERO, Optional.empty())))
+ );
+ ret.add(new TestCase("two segments, one mis-referenced")
+ .balanceSegmentEntities(Arrays.asList(
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "first", BigDecimal.ZERO),
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "second", BigDecimal.TEN)))
+ .fromSegment("first")
+ .toSegment("second2")
+ .expectedResult(Optional.empty())
+ );
+ return ret;
+ }
+
+ private final ScheduledChargesServiceTest.TestCase testCase;
+
+ public ScheduledChargesServiceTest(final ScheduledChargesServiceTest.TestCase testCase) {
+ this.testCase = testCase;
+ }
+
+ @Test
+ public void findChargeRange() throws Exception {
+
+ final ChargeDefinitionService chargeDefinitionServiceMock = Mockito.mock(ChargeDefinitionService.class);
+ final BalanceSegmentRepository balanceSegmentRepositoryMock = Mockito.mock(BalanceSegmentRepository.class);
+
+ Mockito.doReturn(testCase.balanceSegmentEntities.stream())
+ .when(balanceSegmentRepositoryMock)
+ .findByProductIdentifierAndSegmentSetIdentifier(PRODUCT_IDENTIFIER, SEGMENT_SET_IDENTIFIER);
+
+ final ScheduledChargesService testSubject = new ScheduledChargesService(chargeDefinitionServiceMock, balanceSegmentRepositoryMock);
+ final ChargeDefinition chargeDefinition = new ChargeDefinition();
+
+ chargeDefinition.setForSegmentSet(SEGMENT_SET_IDENTIFIER);
+ chargeDefinition.setFromSegment(testCase.fromSegment);
+ chargeDefinition.setToSegment(testCase.toSegment);
+ final Optional<ChargeRange> result = testSubject.findChargeRange(PRODUCT_IDENTIFIER, chargeDefinition);
+ Assert.assertEquals(testCase.expectedResult, result);
+ }
+}
\ No newline at end of file