Merge pull request #16 from myrlen/develop
fixing edge cases
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 1362783..0c6b96c 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -310,7 +310,7 @@
AccountAssignment assignEntryToTeller() {
final AccountAssignment entryAccountAssignment = new AccountAssignment();
entryAccountAssignment.setDesignator(AccountDesignators.ENTRY);
- entryAccountAssignment.setAccountIdentifier(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER);
+ entryAccountAssignment.setAccountIdentifier(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
return entryAccountAssignment;
}
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 13ecedd..4665c8b 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -60,7 +60,7 @@
static final String LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER = "1310";
static final String PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER = "1312";
static final String DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER = "1313";
- static final String TELLER_ONE_ACCOUNT_IDENTIFIER = "7352";
+ static final String CUSTOMERS_DEPOSIT_ACCOUNT = "7352";
static final String LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER = "7810";
static final String CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER = "1103";
static final String LATE_FEE_INCOME_ACCOUNT_IDENTIFIER = "1311";
@@ -204,7 +204,7 @@
private static Account tellerOneAccount() {
final Account ret = new Account();
- ret.setIdentifier(TELLER_ONE_ACCOUNT_IDENTIFIER);
+ ret.setIdentifier(CUSTOMERS_DEPOSIT_ACCOUNT);
ret.setLedger(CASH_LEDGER_IDENTIFIER);
ret.setType(AccountType.ASSET.name());
return ret;
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 9ac056b..d4d2c27 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -336,6 +336,7 @@
private BigDecimal findNextRepaymentAmount(
final LocalDateTime forDateTime) {
+ AccountingFixture.mockBalance(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT, BigDecimal.valueOf(2000_00L, 2));
final Payment nextPayment = portfolioManager.getCostComponentsForAction(
product.getIdentifier(),
customerCase.getIdentifier(),
@@ -551,7 +552,7 @@
debtors.add(new Debtor(AccountingFixture.GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER, provisionForLosses.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
- creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, amount.toString()));
+ creditors.add(new Creditor(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT, amount.toString()));
creditors.add(new Creditor(AccountingFixture.PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER, PROCESSING_FEE_AMOUNT.toPlainString()));
creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, disbursementFeeAmount.toPlainString()));
creditors.add(new Creditor(AccountingFixture.LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER, LOAN_ORIGINATION_FEE_AMOUNT.toPlainString()));
@@ -869,6 +870,8 @@
logger.info("step7PaybackPartialAmount '{}' '{}'", amount, forDateTime);
final BigDecimal principal = amount.subtract(interestAccrued).subtract(lateFee.add(nonLateFees));
+ AccountingFixture.mockBalance(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT, amount);
+
final Payment payment = checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
@@ -906,7 +909,7 @@
if (lateFee.compareTo(BigDecimal.ZERO) != 0) {
debtors.add(new Debtor(AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER, lateFee.toPlainString()));
}
- debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, tellerOneDebit.toPlainString()));
+ debtors.add(new Debtor(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT, tellerOneDebit.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
creditors.add(new Creditor(customerLoanPrincipalIdentifier, principal.toPlainString()));
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 ef3812d..7b06cec 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
@@ -154,7 +154,7 @@
final RealRunningBalances balances = new RealRunningBalances(accountingAdapter, dataContextOfAction);
- final BigDecimal currentBalance = balances.getAccountBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ final BigDecimal currentBalance = balances.getAccountBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL).orElse(BigDecimal.ZERO);
if (currentBalance.compareTo(BigDecimal.ZERO) == 0) //No late fees if the current balance is zilch.
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 4f37edd..9d1ff62 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
@@ -353,7 +353,7 @@
customerCase.setCurrentState(Case.State.ACTIVE.name());
caseRepository.save(customerCase);
}
- final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP);
+ final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO);
final BigDecimal newLoanPaymentSize = disbursePaymentBuilderService.getLoanPaymentSizeForSingleDisbursement(
currentBalance.add(paymentBuilder.getBalanceAdjustment(AccountDesignators.ENTRY)),
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 9ae6402..29e994e 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
@@ -19,6 +19,7 @@
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.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.internal.service.costcomponent.CostComponentService;
import io.mifos.individuallending.internal.service.costcomponent.PaymentBuilder;
@@ -150,6 +151,8 @@
requestedDisbursal = BigDecimal.ZERO;
}
+ balances.adjustBalance(AccountDesignators.ENTRY, requestedRepayment);
+
final SortedSet<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
final PaymentBuilder paymentBuilder =
CostComponentService.getCostComponentsForScheduledCharges(
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderService.java
index d3bed9b..8c52c22 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderService.java
@@ -71,21 +71,25 @@
if (requestedLoanPaymentSize != null) {
loanPaymentSize = requestedLoanPaymentSize
- .min(runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP));
+ .min(runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO));
}
else if (scheduledAction.getActionPeriod() != null && scheduledAction.getActionPeriod().isLastPeriod()) {
- loanPaymentSize = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP);
+ loanPaymentSize = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO);
}
else {
loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize()
- .min(runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP));
+ .min(runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO));
}
+ final AvailableRunningBalancesWithLimits availableRunningBalanceWithLimits =
+ new AvailableRunningBalancesWithLimits(runningBalances);
+ availableRunningBalanceWithLimits.setUpperLimit(AccountDesignators.ENTRY, loanPaymentSize);
+
return CostComponentService.getCostComponentsForScheduledCharges(
scheduledChargesForThisAction,
caseParameters.getBalanceRangeMaximum(),
- runningBalances,
+ availableRunningBalanceWithLimits,
dataContextOfAction.getCaseParametersEntity().getPaymentSize(),
BigDecimal.ZERO,
loanPaymentSize,
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AvailableRunningBalancesWithLimits.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AvailableRunningBalancesWithLimits.java
new file mode 100644
index 0000000..3f8a072
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AvailableRunningBalancesWithLimits.java
@@ -0,0 +1,67 @@
+/*
+ * 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.costcomponent;
+
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+public class AvailableRunningBalancesWithLimits implements RunningBalances {
+ private final RunningBalances decoratedRunningBalances;
+
+ private final Map<String, BigDecimal> upperLimits = new HashMap<>();
+
+ AvailableRunningBalancesWithLimits(final RunningBalances decoratedRunningBalances) {
+ this.decoratedRunningBalances = decoratedRunningBalances;
+ }
+
+ void setUpperLimit(final String designator, final BigDecimal limit) {
+ upperLimits.put(designator, limit);
+ }
+
+ @Override
+ public BigDecimal getAvailableBalance(final String designator, final BigDecimal requestedAmount) {
+ final BigDecimal balance = getBalance(designator).orElse(requestedAmount);
+ final BigDecimal upperLimit = upperLimits.get(designator);
+ if (upperLimit == null)
+ return balance;
+ else
+ return upperLimit.min(balance);
+ }
+
+ @Override
+ public Optional<BigDecimal> getAccountBalance(final String accountDesignator) {
+ return decoratedRunningBalances.getAccountBalance(accountDesignator);
+ }
+
+ @Override
+ public BigDecimal getAccruedBalanceForCharge(final ChargeDefinition chargeDefinition) {
+ return decoratedRunningBalances.getAccruedBalanceForCharge(chargeDefinition);
+ }
+
+ @Override
+ public Optional<LocalDateTime> getStartOfTerm(final DataContextOfAction dataContextOfAction) {
+ return decoratedRunningBalances.getStartOfTerm(dataContextOfAction);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ClosePaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ClosePaymentBuilderService.java
index 1b37c95..d759022 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ClosePaymentBuilderService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ClosePaymentBuilderService.java
@@ -52,7 +52,7 @@
final LocalDate forDate,
final RunningBalances runningBalances)
{
- if (runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).compareTo(BigDecimal.ZERO) != 0)
+ if (runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO).compareTo(BigDecimal.ZERO) != 0)
throw ServiceException.conflict("Cannot close loan until the balance is zero.");
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentService.java
index 6d7b0ca..91f90b7 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentService.java
@@ -131,11 +131,11 @@
case MAXIMUM_BALANCE_DESIGNATOR:
return maximumBalance;
case RUNNING_BALANCE_DESIGNATOR: {
- final BigDecimal customerLoanRunningBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP);
+ final BigDecimal customerLoanRunningBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).orElse(BigDecimal.ZERO);
return customerLoanRunningBalance.subtract(paymentBuilder.getBalanceAdjustment(AccountDesignators.CUSTOMER_LOAN_GROUP));
}
case PRINCIPAL_DESIGNATOR: {
- return runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ return runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL).orElse(BigDecimal.ZERO);
}
case CONTRACTUAL_REPAYMENT_DESIGNATOR:
return contractualRepayment;
@@ -144,10 +144,10 @@
case REQUESTED_REPAYMENT_DESIGNATOR:
return requestedRepayment.add(paymentBuilder.getBalanceAdjustment(AccountDesignators.ENTRY));
case TO_ACCOUNT_DESIGNATOR:
- return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getToAccountDesignator())
+ return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getToAccountDesignator()).orElse(BigDecimal.ZERO)
.subtract(paymentBuilder.getBalanceAdjustment(scheduledCharge.getChargeDefinition().getToAccountDesignator()));
case FROM_ACCOUNT_DESIGNATOR:
- return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getFromAccountDesignator())
+ return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getFromAccountDesignator()).orElse(BigDecimal.ZERO)
.add(paymentBuilder.getBalanceAdjustment(scheduledCharge.getChargeDefinition().getFromAccountDesignator()));
default:
return BigDecimal.ZERO;
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderService.java
index c843ee0..2a00590 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderService.java
@@ -55,7 +55,7 @@
final LocalDate forDate,
final RunningBalances runningBalances)
{
- final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL).orElse(BigDecimal.ZERO);
if (requestedDisbursalSize != null &&
dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum().compareTo(
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java
index 2821285..30b05be 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java
@@ -37,7 +37,7 @@
private final AccountingAdapter accountingAdapter;
private final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper;
private final DataContextOfAction dataContextOfAction;
- private final ExpiringMap<String, BigDecimal> realAccountBalanceCache;
+ private final ExpiringMap<String, Optional<BigDecimal>> realAccountBalanceCache;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private Optional<LocalDateTime> startOfTerm;
@@ -60,14 +60,14 @@
else {
accountIdentifier = Optional.of(designatorToAccountIdentifierMapper.mapOrThrow(accountDesignator));
}
- return accountIdentifier.map(accountingAdapter::getCurrentAccountBalance).orElse(BigDecimal.ZERO);
+ return accountIdentifier.map(accountingAdapter::getCurrentAccountBalance);
})
.build();
this.startOfTerm = Optional.empty();
}
@Override
- public BigDecimal getAccountBalance(final String accountDesignator) {
+ public Optional<BigDecimal> getAccountBalance(final String accountDesignator) {
return realAccountBalanceCache.get(accountDesignator);
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java
index 580293f..affd152 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java
@@ -55,7 +55,7 @@
//TODO: derive signs from IndividualLendingPatternFactory.individualLendingRequiredAccounts instead.
}};
- BigDecimal getAccountBalance(final String accountDesignator);
+ Optional<BigDecimal> getAccountBalance(final String accountDesignator);
BigDecimal getAccruedBalanceForCharge(
final ChargeDefinition chargeDefinition);
@@ -69,16 +69,26 @@
dataContextOfAction.getCompoundIdentifer()));
}
- default BigDecimal getLedgerBalance(final String ledgerDesignator) {
+ default Optional<BigDecimal> getLedgerBalance(final String ledgerDesignator) {
final Pattern individualLendingPattern = IndividualLendingPatternFactory.individualLendingPattern();
return individualLendingPattern.getAccountAssignmentsRequired().stream()
.filter(requiredAccountAssignment -> ledgerDesignator.equals(requiredAccountAssignment.getGroup()))
.map(RequiredAccountAssignment::getAccountDesignator)
.map(this::getAccountBalance)
- .reduce(BigDecimal.ZERO, BigDecimal::add);
+ .reduce(Optional.empty(), (x, y) -> {
+ if (x.isPresent() && y.isPresent())
+ return Optional.of(x.get().add(y.get()));
+ else if (x.isPresent())
+ return x;
+ else //noinspection OptionalIsPresent
+ if (y.isPresent())
+ return y;
+ else
+ return Optional.empty();
+ });
}
- default BigDecimal getBalance(final String designator) {
+ default Optional<BigDecimal> getBalance(final String designator) {
final Pattern individualLendingPattern = IndividualLendingPatternFactory.individualLendingPattern();
if (individualLendingPattern.getAccountAssignmentGroups().contains(designator))
return getLedgerBalance(designator);
@@ -86,28 +96,35 @@
return getAccountBalance(designator);
}
- default BigDecimal getMaxDebit(final String accountDesignator, final BigDecimal amount) {
- if (accountDesignator.equals(AccountDesignators.ENTRY) ||
- accountDesignator.equals(AccountDesignators.PRODUCT_LOSS_ALLOWANCE))
- return amount;
+ /**
+ *
+ * @param requestedAmount The requested amount is necessary as a parameter, because infinity is
+ * not available as a return value for BigDecimal. There is no way to express that there is
+ * no limit, so when there is no limit, the requestedAmount is what is returned.
+ */
+ default BigDecimal getAvailableBalance(final String designator, final BigDecimal requestedAmount) {
+ return getBalance(designator).orElse(requestedAmount);
+ }
+
+ default BigDecimal getMaxDebit(final String accountDesignator, final BigDecimal amount) {
if (ACCOUNT_SIGNS.get(accountDesignator).signum() == -1)
return amount;
else
- return amount.min(getBalance(accountDesignator));
+ return amount.min(getAvailableBalance(accountDesignator, amount));
}
default BigDecimal getMaxCredit(final String accountDesignator, final BigDecimal amount) {
- if (accountDesignator.equals(AccountDesignators.ENTRY) ||
- accountDesignator.equals(AccountDesignators.EXPENSE) ||
- accountDesignator.equals(AccountDesignators.PRODUCT_LOSS_ALLOWANCE))
+ if (accountDesignator.equals(AccountDesignators.EXPENSE) ||
+ accountDesignator.equals(AccountDesignators.PRODUCT_LOSS_ALLOWANCE) ||
+ accountDesignator.equals(AccountDesignators.GENERAL_LOSS_ALLOWANCE))
return amount;
- //entry account can achieve a "relative" negative balance, and
- // product loss allowance can achieve an "absolute" negative balance.
+ //expense account can achieve a "relative" negative balance, and
+ // both loss allowance accounts can achieve an "absolute" negative balance.
if (ACCOUNT_SIGNS.get(accountDesignator).signum() != -1)
return amount;
else
- return amount.min(getBalance(accountDesignator));
+ return amount.min(getAvailableBalance(accountDesignator, amount));
}
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/SimulatedRunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/SimulatedRunningBalances.java
index fcd921d..cf7d47c 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/SimulatedRunningBalances.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/SimulatedRunningBalances.java
@@ -42,8 +42,8 @@
}
@Override
- public BigDecimal getAccountBalance(final String accountDesignator) {
- return balances.getOrDefault(accountDesignator, BigDecimal.ZERO);
+ public Optional<BigDecimal> getAccountBalance(final String accountDesignator) {
+ return Optional.ofNullable(balances.get(accountDesignator));
}
@Override
@@ -58,7 +58,7 @@
return Optional.ofNullable(startOfTerm);
}
- void adjustBalance(final String key, final BigDecimal amount) {
+ public void adjustBalance(final String key, final BigDecimal amount) {
final BigDecimal sign = ACCOUNT_SIGNS.get(key);
final BigDecimal currentValue = balances.getOrDefault(key, BigDecimal.ZERO);
final BigDecimal newValue = currentValue.add(amount.multiply(sign));
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java
index 8fbcda1..96add20 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java
@@ -25,6 +25,9 @@
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -40,16 +43,53 @@
@Parameterized.Parameters
public static Collection testCases() {
final Collection<PaymentBuilderServiceTestCase> ret = new ArrayList<>();
- ret.add(simpleCase());
+ ret.add(new PaymentBuilderServiceTestCase("simple case"));
+ ret.add(disbursementFeesExceedFirstRepayment());
+ ret.add(lastLittleRepaymentZerosPrincipal());
+ ret.add(lastBigRepaymentZerosPrincipal());
+ ret.add(explicitlySetRepaymentSizeIsSmallerThanFees());
return ret;
}
- private static PaymentBuilderServiceTestCase simpleCase() {
- final PaymentBuilderServiceTestCase testCase = new PaymentBuilderServiceTestCase("simple case");
- testCase.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, testCase.balance.negate());
- testCase.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_INTEREST, testCase.accruedInterest.negate());
- testCase.runningBalances.adjustBalance(AccountDesignators.INTEREST_ACCRUAL, testCase.accruedInterest);
- return testCase;
+ private static PaymentBuilderServiceTestCase disbursementFeesExceedFirstRepayment() {
+ return new PaymentBuilderServiceTestCase("disbursement fees exceed first repayment")
+ .nonLateFees(BigDecimal.valueOf(200_00, 2))
+ .expectedPrincipalRepayment(BigDecimal.ZERO)
+ .expectedFeeRepayment(BigDecimal.valueOf(90_00, 2));
+ }
+
+ private static PaymentBuilderServiceTestCase lastLittleRepaymentZerosPrincipal() {
+ return new PaymentBuilderServiceTestCase("last repayment should zero principal, although standard repayment larger than principal")
+ .nonLateFees(BigDecimal.ZERO)
+ .endOfTerm(LocalDateTime.now(Clock.systemUTC()))
+ .forDate(LocalDateTime.now(Clock.systemUTC()))
+ .requestedPaymentSize(null)
+ .remainingPrincipal(BigDecimal.valueOf(42_00, 2))
+ .expectedPrincipalRepayment(BigDecimal.valueOf(42_00, 2))
+ .expectedInterestRepayment(BigDecimal.valueOf(10_00, 2))
+ .expectedFeeRepayment(BigDecimal.ZERO);
+ }
+
+ private static PaymentBuilderServiceTestCase lastBigRepaymentZerosPrincipal() {
+ return new PaymentBuilderServiceTestCase("last repayment should zero principal, although standard repayment smaller than principal")
+ .nonLateFees(BigDecimal.ZERO)
+ .endOfTerm(LocalDateTime.now(Clock.systemUTC()))
+ .forDate(LocalDateTime.now(Clock.systemUTC()))
+ .requestedPaymentSize(null)
+ .remainingPrincipal(BigDecimal.valueOf(142_00, 2))
+ .expectedPrincipalRepayment(BigDecimal.valueOf(142_00, 2))
+ .expectedInterestRepayment(BigDecimal.valueOf(10_00, 2))
+ .expectedFeeRepayment(BigDecimal.ZERO);
+ }
+
+ private static PaymentBuilderServiceTestCase explicitlySetRepaymentSizeIsSmallerThanFees() {
+ return new PaymentBuilderServiceTestCase("a payment size was chosen which is smaller than the fees due.")
+ .nonLateFees(BigDecimal.valueOf(50_00, 2))
+ .accruedInterest(BigDecimal.ZERO)
+ .requestedPaymentSize(BigDecimal.valueOf(49_00, 2))
+ .expectedInterestRepayment(BigDecimal.ZERO)
+ .expectedPrincipalRepayment(BigDecimal.ZERO)
+ .expectedFeeRepayment(BigDecimal.valueOf(49_00, 2));
}
private final PaymentBuilderServiceTestCase testCase;
@@ -63,13 +103,28 @@
final PaymentBuilder paymentBuilder = PaymentBuilderServiceTestHarness.constructCallToPaymentBuilder(
AcceptPaymentBuilderService::new, testCase);
- final Payment payment = paymentBuilder.buildPayment(Action.ACCEPT_PAYMENT, Collections.emptySet(), testCase.forDate.toLocalDate());
- Assert.assertNotNull(payment);
- final Map<String, CostComponent> mappedCostComponents = payment.getCostComponents().stream()
- .collect(Collectors.toMap(CostComponent::getChargeIdentifier, x -> x));
+ final Payment payment = paymentBuilder.buildPayment(
+ Action.ACCEPT_PAYMENT,
+ Collections.emptySet(),
+ testCase.forDate.toLocalDate());
- Assert.assertEquals(testCase.accruedInterest, mappedCostComponents.get(ChargeIdentifiers.INTEREST_ID).getAmount());
- Assert.assertEquals(testCase.accruedInterest, mappedCostComponents.get(ChargeIdentifiers.REPAY_INTEREST_ID).getAmount());
- Assert.assertEquals(testCase.paymentSize.subtract(testCase.accruedInterest), mappedCostComponents.get(ChargeIdentifiers.REPAY_PRINCIPAL_ID).getAmount());
+ Assert.assertNotNull(payment);
+ final Map<String, BigDecimal> mappedCostComponents = payment.getCostComponents().stream()
+ .collect(Collectors.toMap(CostComponent::getChargeIdentifier, CostComponent::getAmount));
+
+ Assert.assertEquals(testCase.toString(),
+ testCase.expectedInterestRepayment, mappedCostComponents.getOrDefault(ChargeIdentifiers.INTEREST_ID, BigDecimal.ZERO));
+ Assert.assertEquals(testCase.toString(),
+ testCase.expectedInterestRepayment, mappedCostComponents.getOrDefault(ChargeIdentifiers.REPAY_INTEREST_ID, BigDecimal.ZERO));
+ Assert.assertEquals(testCase.toString(),
+ testCase.expectedPrincipalRepayment, mappedCostComponents.getOrDefault(ChargeIdentifiers.REPAY_PRINCIPAL_ID, BigDecimal.ZERO));
+ Assert.assertEquals(testCase.toString(),
+ testCase.expectedFeeRepayment, mappedCostComponents.getOrDefault(ChargeIdentifiers.REPAY_FEES_ID, BigDecimal.ZERO));
+
+ final BigDecimal expectedTotalRepaymentSize = testCase.expectedFeeRepayment.add(testCase.expectedInterestRepayment).add(testCase.expectedPrincipalRepayment);
+ Assert.assertEquals(expectedTotalRepaymentSize.negate(), payment.getBalanceAdjustments().getOrDefault(AccountDesignators.ENTRY, BigDecimal.ZERO));
+ Assert.assertEquals(testCase.expectedFeeRepayment, payment.getBalanceAdjustments().getOrDefault(AccountDesignators.CUSTOMER_LOAN_FEES, BigDecimal.ZERO));
+ Assert.assertEquals(testCase.expectedInterestRepayment, payment.getBalanceAdjustments().getOrDefault(AccountDesignators.CUSTOMER_LOAN_INTEREST, BigDecimal.ZERO));
+ Assert.assertEquals(testCase.expectedPrincipalRepayment, payment.getBalanceAdjustments().getOrDefault(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, BigDecimal.ZERO));
}
}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java
index 80ba2a7..7d2152f 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java
@@ -31,7 +31,6 @@
@Test
public void getPaymentBuilder() throws Exception {
final PaymentBuilderServiceTestCase testCase = new PaymentBuilderServiceTestCase("simple case");
- testCase.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, testCase.balance.negate());
final PaymentBuilder paymentBuilder = PaymentBuilderServiceTestHarness.constructCallToPaymentBuilder(
ApplyInterestPaymentBuilderService::new, testCase);
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderServiceTest.java
index 4d3d907..246fb64 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderServiceTest.java
@@ -71,13 +71,13 @@
.collect(Collectors.toMap(CostComponent::getChargeIdentifier, x -> x));
Assert.assertEquals(
- testCase.paymentSize,
+ testCase.configuredPaymentSize,
mappedCostComponents.get(ChargeIdentifiers.DISBURSE_PAYMENT_ID).getAmount());
Assert.assertEquals(
- testCase.paymentSize.multiply(BigDecimal.valueOf(1, 2)).setScale(2, BigDecimal.ROUND_HALF_EVEN),
+ testCase.configuredPaymentSize.multiply(BigDecimal.valueOf(1, 2)).setScale(2, BigDecimal.ROUND_HALF_EVEN),
paymentBuilder.getBalanceAdjustments().get(AccountDesignators.PRODUCT_LOSS_ALLOWANCE));
Assert.assertEquals(
- testCase.paymentSize.multiply(BigDecimal.valueOf(1, 2)).negate().setScale(2, BigDecimal.ROUND_HALF_EVEN),
+ testCase.configuredPaymentSize.multiply(BigDecimal.valueOf(1, 2)).negate().setScale(2, BigDecimal.ROUND_HALF_EVEN),
paymentBuilder.getBalanceAdjustments().get(AccountDesignators.GENERAL_LOSS_ALLOWANCE));
}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestCase.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestCase.java
index 76bae20..29748f8 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestCase.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestCase.java
@@ -21,19 +21,24 @@
class PaymentBuilderServiceTestCase {
private final String description;
- private LocalDateTime startOfTerm = LocalDateTime.of(2015, 1, 15, 0, 0);
+ LocalDateTime startOfTerm = LocalDateTime.of(2015, 1, 15, 0, 0);
LocalDateTime endOfTerm = LocalDate.of(2015, 8, 15).atStartOfDay();
LocalDateTime forDate = startOfTerm.plusMonths(1);
- BigDecimal paymentSize = BigDecimal.valueOf(100_00, 2);
- BigDecimal balance = BigDecimal.valueOf(2000_00, 2);
- BigDecimal balanceRangeMaximum = BigDecimal.valueOf(1000_00, 2);
+ BigDecimal configuredPaymentSize = BigDecimal.valueOf(100_00, 2);
+ BigDecimal requestedPaymentSize = BigDecimal.valueOf(100_00, 2);
+ BigDecimal entryAccountBalance = BigDecimal.valueOf(10_000_00, 2);
+ BigDecimal remainingPrincipal = BigDecimal.valueOf(2000_00, 2);
+ BigDecimal balanceRangeMaximum = BigDecimal.valueOf(4000_00, 2);
BigDecimal interestRate = BigDecimal.valueOf(5_00, 2);
BigDecimal accruedInterest = BigDecimal.valueOf(10_00, 2);
- SimulatedRunningBalances runningBalances;
+ BigDecimal nonLateFees = BigDecimal.valueOf(10_00, 2);
+ BigDecimal expectedPrincipalRepayment = BigDecimal.valueOf(80_00, 2);
+ BigDecimal expectedFeeRepayment = BigDecimal.valueOf(10_00, 2);
+ BigDecimal expectedInterestRepayment = BigDecimal.valueOf(10_00, 2);
+ BigDecimal generalLossAllowance = BigDecimal.valueOf(2000_00, 2);
PaymentBuilderServiceTestCase(final String description) {
this.description = description;
- runningBalances = new SimulatedRunningBalances(startOfTerm);
}
PaymentBuilderServiceTestCase endOfTerm(LocalDateTime endOfTerm) {
@@ -46,23 +51,13 @@
return this;
}
- PaymentBuilderServiceTestCase paymentSize(BigDecimal paymentSize) {
- this.paymentSize = paymentSize;
+ PaymentBuilderServiceTestCase requestedPaymentSize(BigDecimal newVal) {
+ this.requestedPaymentSize = newVal;
return this;
}
- PaymentBuilderServiceTestCase balance(BigDecimal balance) {
- this.balance = balance;
- return this;
- }
-
- PaymentBuilderServiceTestCase balanceRangeMaximum(BigDecimal balanceRangeMaximum) {
- this.balanceRangeMaximum = balanceRangeMaximum;
- return this;
- }
-
- PaymentBuilderServiceTestCase interestRate(BigDecimal interestRate) {
- this.interestRate = interestRate;
+ PaymentBuilderServiceTestCase remainingPrincipal(BigDecimal newVal) {
+ this.remainingPrincipal = newVal;
return this;
}
@@ -71,8 +66,28 @@
return this;
}
- PaymentBuilderServiceTestCase runningBalances(SimulatedRunningBalances newVal) {
- this.runningBalances = newVal;
+ PaymentBuilderServiceTestCase nonLateFees(BigDecimal newVal) {
+ this.nonLateFees = newVal;
+ return this;
+ }
+
+ PaymentBuilderServiceTestCase expectedPrincipalRepayment(BigDecimal newVal) {
+ this.expectedPrincipalRepayment = newVal;
+ return this;
+ }
+
+ PaymentBuilderServiceTestCase expectedFeeRepayment(BigDecimal newVal) {
+ this.expectedFeeRepayment = newVal;
+ return this;
+ }
+
+ PaymentBuilderServiceTestCase expectedInterestRepayment(BigDecimal newVal) {
+ this.expectedInterestRepayment = newVal;
+ return this;
+ }
+
+ PaymentBuilderServiceTestCase generalLossAllowance(BigDecimal newVal) {
+ this.generalLossAllowance = newVal;
return this;
}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestHarness.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestHarness.java
index 99a5c17..3496385 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestHarness.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestHarness.java
@@ -15,6 +15,7 @@
*/
package io.mifos.individuallending.internal.service.costcomponent;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.internal.repository.CaseParametersEntity;
import io.mifos.individuallending.internal.service.ChargeDefinitionService;
import io.mifos.individuallending.internal.service.DataContextOfAction;
@@ -45,12 +46,21 @@
customerCase.setEndOfTerm(testCase.endOfTerm);
customerCase.setInterest(testCase.interestRate);
final CaseParametersEntity caseParameters = new CaseParametersEntity();
- caseParameters.setPaymentSize(testCase.paymentSize);
+ caseParameters.setPaymentSize(testCase.configuredPaymentSize);
caseParameters.setBalanceRangeMaximum(testCase.balanceRangeMaximum);
caseParameters.setPaymentCyclePeriod(1);
caseParameters.setPaymentCycleTemporalUnit(ChronoUnit.MONTHS);
caseParameters.setCreditWorthinessFactors(Collections.emptySet());
+ final SimulatedRunningBalances runningBalances = new SimulatedRunningBalances(testCase.startOfTerm);
+ runningBalances.adjustBalance(AccountDesignators.ENTRY, testCase.entryAccountBalance);
+ runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, testCase.remainingPrincipal.negate());
+ runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_INTEREST, testCase.accruedInterest.negate());
+ runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_FEES, testCase.nonLateFees.negate());
+ runningBalances.adjustBalance(AccountDesignators.INTEREST_ACCRUAL, testCase.accruedInterest);
+
+ runningBalances.adjustBalance(AccountDesignators.GENERAL_LOSS_ALLOWANCE, testCase.generalLossAllowance.negate());
+
final DataContextOfAction dataContextOfAction = new DataContextOfAction(
product,
customerCase,
@@ -58,8 +68,8 @@
Collections.emptyList());
return testSubject.getPaymentBuilder(
dataContextOfAction,
- testCase.paymentSize,
+ testCase.requestedPaymentSize,
testCase.forDate.toLocalDate(),
- testCase.runningBalances);
+ runningBalances);
}
}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java
index fe5f7d4..46e2dbc 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java
@@ -15,7 +15,6 @@
*/
package io.mifos.individuallending.internal.service.costcomponent;
-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.internal.service.DefaultChargeDefinitionsMocker;
@@ -26,6 +25,7 @@
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;
@@ -41,16 +41,14 @@
@Parameterized.Parameters
public static Collection testCases() {
final Collection<PaymentBuilderServiceTestCase> ret = new ArrayList<>();
- ret.add(simpleCase());
- //TODO: add use case for when the general loss allowance account doesn't have enough to cover the write off.
+ ret.add(new PaymentBuilderServiceTestCase("simple case"));
+ ret.add(lossProvisioningInsufficient());
return ret;
}
- private static PaymentBuilderServiceTestCase simpleCase() {
- final PaymentBuilderServiceTestCase ret = new PaymentBuilderServiceTestCase("simple case");
- ret.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, ret.balance.negate());
- ret.runningBalances.adjustBalance(AccountDesignators.GENERAL_LOSS_ALLOWANCE, ret.balance.negate());
- return ret;
+ private static PaymentBuilderServiceTestCase lossProvisioningInsufficient() {
+ return new PaymentBuilderServiceTestCase("loss provisioning insufficient")
+ .generalLossAllowance(BigDecimal.ZERO);
}
private final PaymentBuilderServiceTestCase testCase;
@@ -70,8 +68,7 @@
.collect(Collectors.toMap(CostComponent::getChargeIdentifier, x -> x));
Assert.assertEquals(
- testCase.balance,
+ testCase.remainingPrincipal,
mappedCostComponents.get(ChargeIdentifiers.WRITE_OFF_ID).getAmount());
}
-
}