List<CostComponents> -> Payment object
Corresponding changes to PlannedPayment
Changes to charges and required account assignments.
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/Balance.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/Balance.java
new file mode 100644
index 0000000..a8803d4
--- /dev/null
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/Balance.java
@@ -0,0 +1,65 @@
+/*
+ * 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.api.v1.domain.caseinstance;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+public class Balance {
+ private String accountDesignator;
+ private BigDecimal amount;
+
+ public String getAccountDesignator() {
+ return accountDesignator;
+ }
+
+ public void setAccountDesignator(String accountDesignator) {
+ this.accountDesignator = accountDesignator;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ public void setAmount(BigDecimal amount) {
+ this.amount = amount;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Balance balance = (Balance) o;
+ return Objects.equals(accountDesignator, balance.accountDesignator) &&
+ Objects.equals(amount, balance.amount);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(accountDesignator, amount);
+ }
+
+ @Override
+ public String toString() {
+ return "Balance{" +
+ "accountDesignator='" + accountDesignator + '\'' +
+ ", amount=" + amount +
+ '}';
+ }
+}
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/PlannedPayment.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/PlannedPayment.java
index 30c4208..c988347 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/PlannedPayment.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/PlannedPayment.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 The Mifos Initiative.
+ * 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.
@@ -15,62 +15,38 @@
*/
package io.mifos.individuallending.api.v1.domain.caseinstance;
-import io.mifos.portfolio.api.v1.domain.CostComponent;
+import io.mifos.portfolio.api.v1.domain.Payment;
-import javax.annotation.Nullable;
import java.math.BigDecimal;
-import java.util.List;
+import java.util.Map;
import java.util.Objects;
/**
* @author Myrle Krantz
*/
-@SuppressWarnings({"WeakerAccess", "unused"})
-public final class PlannedPayment {
- private Double interestRate;
- private List<CostComponent> costComponents;
- private BigDecimal remainingPrincipal;
- private @Nullable String date;
+public class PlannedPayment {
+ private Payment payment;
+ private Map<String, BigDecimal> balances;
- public PlannedPayment() {
+ public PlannedPayment(Payment payment, Map<String, BigDecimal> balances) {
+ this.payment = payment;
+ this.balances = balances;
}
- public PlannedPayment(Double interestRate, List<CostComponent> costComponents, BigDecimal remainingPrincipal) {
- this.interestRate = interestRate;
- this.costComponents = costComponents;
- this.remainingPrincipal = remainingPrincipal;
+ public Payment getPayment() {
+ return payment;
}
- public Double getInterestRate() {
- return interestRate;
+ public void setPayment(Payment payment) {
+ this.payment = payment;
}
- public void setInterestRate(Double interestRate) {
- this.interestRate = interestRate;
+ public Map<String, BigDecimal> getBalances() {
+ return balances;
}
- public List<CostComponent> getCostComponents() {
- return costComponents;
- }
-
- public void setCostComponents(List<CostComponent> costComponents) {
- this.costComponents = costComponents;
- }
-
- public BigDecimal getRemainingPrincipal() {
- return remainingPrincipal;
- }
-
- public void setRemainingPrincipal(BigDecimal remainingPrincipal) {
- this.remainingPrincipal = remainingPrincipal;
- }
-
- public String getDate() {
- return date;
- }
-
- public void setDate(String date) {
- this.date = date;
+ public void setBalances(Map<String, BigDecimal> balances) {
+ this.balances = balances;
}
@Override
@@ -78,24 +54,20 @@
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PlannedPayment that = (PlannedPayment) o;
- return Objects.equals(interestRate, that.interestRate) &&
- Objects.equals(costComponents, that.costComponents) &&
- Objects.equals(remainingPrincipal, that.remainingPrincipal) &&
- Objects.equals(date, that.date);
+ return Objects.equals(payment, that.payment) &&
+ Objects.equals(balances, that.balances);
}
@Override
public int hashCode() {
- return Objects.hash(interestRate, costComponents, remainingPrincipal, date);
+ return Objects.hash(payment, balances);
}
@Override
public String toString() {
return "PlannedPayment{" +
- "interestRate=" + interestRate +
- ", costComponents=" + costComponents +
- ", remainingPrincipal=" + remainingPrincipal +
- ", date='" + date + '\'' +
- '}';
+ "payment=" + payment +
+ ", balances=" + balances +
+ '}';
}
}
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java
index 65bd4e6..912ef35 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java
@@ -21,9 +21,7 @@
@SuppressWarnings("unused")
public interface AccountDesignators {
String CUSTOMER_LOAN = "customer-loan";
- String PENDING_DISBURSAL = "pending-disbursal";
String LOAN_FUNDS_SOURCE = "loan-funds-source";
- String LOANS_PAYABLE = "loans-payable";
String PROCESSING_FEE_INCOME = "processing-fee-income";
String ORIGINATION_FEE_INCOME = "origination-fee-income";
String DISBURSEMENT_FEE_INCOME = "disbursement-fee-income";
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
index 9518a1b..f9fbbd9 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
@@ -22,10 +22,6 @@
*/
@SuppressWarnings("unused")
public interface ChargeIdentifiers {
- String LOAN_FUNDS_ALLOCATION_NAME = "Loan funds allocation";
- String LOAN_FUNDS_ALLOCATION_ID = "loan-funds-allocation";
- String RETURN_DISBURSEMENT_NAME = "Return disbursement";
- String RETURN_DISBURSEMENT_ID = "return-disbursement";
String INTEREST_NAME = "Interest";
String INTEREST_ID = "interest";
String ALLOW_FOR_WRITE_OFF_NAME = "Allow for write-off";
@@ -36,16 +32,12 @@
String DISBURSEMENT_FEE_ID = "disbursement-fee";
String DISBURSE_PAYMENT_NAME = "Disburse payment";
String DISBURSE_PAYMENT_ID = "disburse-payment";
- String TRACK_DISBURSAL_PAYMENT_NAME = "Track disburse payment";
- String TRACK_DISBURSAL_PAYMENT_ID = "track-disburse-payment";
String LOAN_ORIGINATION_FEE_NAME = "Loan origination fee";
String LOAN_ORIGINATION_FEE_ID = "loan-origination-fee";
String PROCESSING_FEE_NAME = "Processing fee";
String PROCESSING_FEE_ID = "processing-fee";
String REPAYMENT_NAME = "Repayment";
String REPAYMENT_ID = "repayment";
- String TRACK_RETURN_PRINCIPAL_NAME = "Track return principal";
- String TRACK_RETURN_PRINCIPAL_ID = "track-return-principal";
static String nameToIdentifier(String name) {
return name.toLowerCase(Locale.US).replace(" ", "-");
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeProportionalDesignator.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeProportionalDesignator.java
index 6f60a57..31cd960 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeProportionalDesignator.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeProportionalDesignator.java
@@ -25,8 +25,9 @@
NOT_PROPORTIONAL("{notproportional}", 0),
MAXIMUM_BALANCE_DESIGNATOR("{maximumbalance}", 1),
RUNNING_BALANCE_DESIGNATOR("{runningbalance}", 2),
- PRINCIPAL_ADJUSTMENT_DESIGNATOR("{principaladjustment}", 3),
- REPAYMENT_DESIGNATOR("{repayment}", 4),
+ REQUESTED_DISBURSEMENT_DESIGNATOR("{requesteddisbursement}", 3),
+ REQUESTED_REPAYMENT_DESIGNATOR("{requestedrepayment}", 4),
+ CONTRACTUAL_REPAYMENT_DESIGNATOR("{contractualrepayment}", 5),
;
private final String value;
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
index ac86d39..7acf0c1 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
@@ -17,6 +17,7 @@
import io.mifos.core.api.annotation.ThrowsException;
import io.mifos.core.api.util.CustomFeignClientsConfiguration;
+import io.mifos.portfolio.api.v1.domain.Payment;
import io.mifos.portfolio.api.v1.domain.*;
import io.mifos.portfolio.api.v1.validation.ValidSortColumn;
import io.mifos.portfolio.api.v1.validation.ValidSortDirection;
@@ -345,7 +346,7 @@
produces = MediaType.ALL_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
- List<CostComponent> getCostComponentsForAction(
+ Payment getCostComponentsForAction(
@PathVariable("productidentifier") final String productIdentifier,
@PathVariable("caseidentifier") final String caseIdentifier,
@PathVariable("actionidentifier") final String actionIdentifier,
@@ -360,7 +361,7 @@
produces = MediaType.ALL_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
- List<CostComponent> getCostComponentsForAction(
+ Payment getCostComponentsForAction(
@PathVariable("productidentifier") final String productIdentifier,
@PathVariable("caseidentifier") final String caseIdentifier,
@PathVariable("actionidentifier") final String actionIdentifier,
@@ -375,7 +376,7 @@
produces = MediaType.ALL_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
- List<CostComponent> getCostComponentsForAction(
+ Payment getCostComponentsForAction(
@PathVariable("productidentifier") final String productIdentifier,
@PathVariable("caseidentifier") final String caseIdentifier,
@PathVariable("actionidentifier") final String actionIdentifier,
@@ -388,7 +389,7 @@
produces = MediaType.ALL_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
- List<CostComponent> getCostComponentsForAction(
+ Payment getCostComponentsForAction(
@PathVariable("productidentifier") final String productIdentifier,
@PathVariable("caseidentifier") final String caseIdentifier,
@PathVariable("actionidentifier") final String actionIdentifier);
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Payment.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Payment.java
new file mode 100644
index 0000000..97d5d51
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Payment.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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.portfolio.api.v1.domain;
+
+import javax.annotation.Nullable;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+public final class Payment {
+ private List<CostComponent> costComponents;
+ private Map<String, BigDecimal> balanceAdjustments;
+ private @Nullable String date;
+
+ public Payment() {
+ }
+
+ public Payment(List<CostComponent> costComponents, Map<String, BigDecimal> balanceAdjustments) {
+ this.costComponents = costComponents;
+ this.balanceAdjustments = balanceAdjustments;
+ }
+
+ public List<CostComponent> getCostComponents() {
+ return costComponents;
+ }
+
+ public void setCostComponents(List<CostComponent> costComponents) {
+ this.costComponents = costComponents;
+ }
+
+ public Map<String, BigDecimal> getBalanceAdjustments() {
+ return balanceAdjustments;
+ }
+
+ public void setBalanceAdjustments(Map<String, BigDecimal> balanceAdjustments) {
+ this.balanceAdjustments = balanceAdjustments;
+ }
+
+ public String getDate() {
+ return date;
+ }
+
+ public void setDate(String date) {
+ this.date = date;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Payment that = (Payment) o;
+ return Objects.equals(costComponents, that.costComponents) &&
+ Objects.equals(balanceAdjustments, that.balanceAdjustments) &&
+ Objects.equals(date, that.date);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(costComponents, balanceAdjustments, date);
+ }
+
+ @Override
+ public String toString() {
+ return "Payment{" +
+ "costComponents=" + costComponents +
+ ", balanceAdjustments=" + balanceAdjustments +
+ ", date='" + date + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/test/java/io/mifos/Fixture.java b/api/src/test/java/io/mifos/Fixture.java
index 5643ecb..a6f1435 100644
--- a/api/src/test/java/io/mifos/Fixture.java
+++ b/api/src/test/java/io/mifos/Fixture.java
@@ -35,15 +35,9 @@
*/
@SuppressWarnings("WeakerAccess")
public class Fixture {
- static final String INCOME_LEDGER_IDENTIFIER = "1000";
- static final String FEES_AND_CHARGES_LEDGER_IDENTIFIER = "1300";
- static final String CASH_LEDGER_IDENTIFIER = "7300";
- static final String PENDING_DISBURSAL_LEDGER_IDENTIFIER = "7320";
- static final String CUSTOMER_LOAN_LEDGER_IDENTIFIER = "7353";
static final String LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER = "7310";
static final String LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER = "1310";
static final String PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER = "1312";
- static final String TELLER_ONE_ACCOUNT_IDENTIFIER = "7352";
static int uniquenessSuffix = 0;
@@ -62,7 +56,6 @@
product.setMinorCurrencyUnitDigits(2);
final Set<AccountAssignment> accountAssignments = new HashSet<>();
- accountAssignments.add(new AccountAssignment(PENDING_DISBURSAL, PENDING_DISBURSAL_LEDGER_IDENTIFIER));
accountAssignments.add(new AccountAssignment(PROCESSING_FEE_INCOME, PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(ORIGINATION_FEE_INCOME, LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(DISBURSEMENT_FEE_INCOME, "001-004"));
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 8ef3fb6..68461f4 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -268,14 +268,14 @@
final Set<String> accountDesignators,
final BigDecimal amount,
final CostComponent... expectedCostComponents) {
- final List<CostComponent> costComponents = portfolioManager.getCostComponentsForAction(
+ final Payment payment = portfolioManager.getCostComponentsForAction(
productIdentifier,
customerCaseIdentifier,
action.name(),
accountDesignators,
amount
);
- final Set<CostComponent> setOfCostComponents = new HashSet<>(costComponents);
+ final Set<CostComponent> setOfCostComponents = new HashSet<>(payment.getCostComponents());
final Set<CostComponent> setOfExpectedCostComponents = Stream.of(expectedCostComponents)
.filter(x -> x.getAmount().compareTo(BigDecimal.ZERO) != 0)
.collect(Collectors.toSet());
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 00e74f7..7df57bf 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -50,7 +50,6 @@
private static final String FEES_AND_CHARGES_LEDGER_IDENTIFIER = "1300";
private static final String ASSET_LEDGER_IDENTIFIER = "7000";
private static final String CASH_LEDGER_IDENTIFIER = "7300";
- static final String PENDING_DISBURSAL_LEDGER_IDENTIFIER = "7320";
static final String CUSTOMER_LOAN_LEDGER_IDENTIFIER = "7353";
private static final String ACCRUED_INCOME_LEDGER_IDENTIFIER = "7800";
@@ -61,7 +60,6 @@
static final String TELLER_ONE_ACCOUNT_IDENTIFIER = "7352";
static final String LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER = "7810";
static final String CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER = "1103";
- static final String LOANS_PAYABLE_ACCOUNT_IDENTIFIER ="8690";
static final String LATE_FEE_INCOME_ACCOUNT_IDENTIFIER = "1311";
static final String LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER = "7840";
static final String ARREARS_ALLOWANCE_ACCOUNT_IDENTIFIER = "3010";
@@ -129,15 +127,6 @@
return ret;
}
- private static Ledger pendingDisbursalLedger() {
- final Ledger ret = new Ledger();
- ret.setIdentifier(PENDING_DISBURSAL_LEDGER_IDENTIFIER);
- ret.setParentLedgerIdentifier(CASH_LEDGER_IDENTIFIER);
- ret.setType(AccountType.ASSET.name());
- ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
- return ret;
- }
-
private static Ledger customerLoanLedger() {
final Ledger ret = new Ledger();
ret.setIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
@@ -223,14 +212,6 @@
return ret;
}
- private static Account loansPayableAccount() {
- final Account ret = new Account();
- ret.setIdentifier(LOANS_PAYABLE_ACCOUNT_IDENTIFIER);
- //ret.setLedger(LOAN_INCOME_LEDGER_IDENTIFIER);
- ret.setType(AccountType.LIABILITY.name());
- return ret;
- }
-
private static Account lateFeeIncomeAccount() {
final Account ret = new Account();
ret.setIdentifier(LATE_FEE_INCOME_ACCOUNT_IDENTIFIER);
@@ -270,23 +251,6 @@
return ret;
}
- private static Object pendingDisbursalAccountsPage() {
- final Account pendingDisbursalAccount1 = new Account();
- pendingDisbursalAccount1.setIdentifier("pendingDisbursalAccount1");
-
- final Account pendingDisbursalAccount2 = new Account();
- pendingDisbursalAccount2.setIdentifier("pendingDisbursalAccount2");
-
- final Account pendingDisbursalAccount3 = new Account();
- pendingDisbursalAccount3.setIdentifier("pendingDisbursalAccount3");
-
- final AccountPage ret = new AccountPage();
- ret.setTotalElements(3L);
- ret.setTotalPages(1);
- ret.setAccounts(Arrays.asList(pendingDisbursalAccount1, pendingDisbursalAccount2, pendingDisbursalAccount3));
- return ret;
- }
-
private static <T> Valid<T> isValid() {
return new Valid<>();
}
@@ -457,7 +421,6 @@
makeAccountResponsive(tellerOneAccount(), universalCreationDate, ledgerManagerMock);
makeAccountResponsive(loanInterestAccrualAccount(), universalCreationDate, ledgerManagerMock);
makeAccountResponsive(consumerLoanInterestAccount(), universalCreationDate, ledgerManagerMock);
- makeAccountResponsive(loansPayableAccount(), universalCreationDate, ledgerManagerMock);
makeAccountResponsive(lateFeeIncomeAccount(), universalCreationDate, ledgerManagerMock);
makeAccountResponsive(lateFeeAccrualAccount(), universalCreationDate, ledgerManagerMock);
makeAccountResponsive(arrearsAllowanceAccount(), universalCreationDate, ledgerManagerMock);
@@ -465,14 +428,11 @@
Mockito.doReturn(incomeLedger()).when(ledgerManagerMock).findLedger(INCOME_LEDGER_IDENTIFIER);
Mockito.doReturn(feesAndChargesLedger()).when(ledgerManagerMock).findLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
Mockito.doReturn(cashLedger()).when(ledgerManagerMock).findLedger(CASH_LEDGER_IDENTIFIER);
- Mockito.doReturn(pendingDisbursalLedger()).when(ledgerManagerMock).findLedger(PENDING_DISBURSAL_LEDGER_IDENTIFIER);
Mockito.doReturn(customerLoanLedger()).when(ledgerManagerMock).findLedger(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
Mockito.doReturn(loanIncomeLedger()).when(ledgerManagerMock).findLedger(LOAN_INCOME_LEDGER_IDENTIFIER);
Mockito.doReturn(accruedIncomeLedger()).when(ledgerManagerMock).findLedger(ACCRUED_INCOME_LEDGER_IDENTIFIER);
Mockito.doReturn(customerLoanAccountsPage()).when(ledgerManagerMock).fetchAccountsOfLedger(Mockito.eq(CUSTOMER_LOAN_LEDGER_IDENTIFIER),
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
- Mockito.doReturn(pendingDisbursalAccountsPage()).when(ledgerManagerMock).fetchAccountsOfLedger(Mockito.eq(PENDING_DISBURSAL_LEDGER_IDENTIFIER),
- Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
Mockito.doAnswer(new FindAccountAnswer()).when(ledgerManagerMock).findAccount(Matchers.anyString());
Mockito.doAnswer(new CreateAccountAnswer()).when(ledgerManagerMock).createAccount(Matchers.any());
@@ -492,20 +452,6 @@
}
static void verifyTransfer(final LedgerManager ledgerManager,
- final String fromAccountIdentifier,
- final String toAccountIdentifier,
- final BigDecimal amount,
- final String productIdentifier,
- final String caseIdentifier,
- final Action action) {
- final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(
- Collections.singleton(new Debtor(fromAccountIdentifier, amount.toPlainString())),
- Collections.singleton(new Creditor(toAccountIdentifier, amount.toPlainString())),
- productIdentifier, caseIdentifier, action);
- Mockito.verify(ledgerManager).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
- }
-
- static void verifyTransfer(final LedgerManager ledgerManager,
final Set<Debtor> debtors,
final Set<Creditor> creditors,
final String productIdentifier,
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 de8fc57..900ac99 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -60,16 +60,11 @@
product.setMinorCurrencyUnitDigits(MINOR_CURRENCY_UNIT_DIGITS);
final Set<AccountAssignment> accountAssignments = new HashSet<>();
- final AccountAssignment pendingDisbursalAccountAssignment = new AccountAssignment();
- pendingDisbursalAccountAssignment.setDesignator(PENDING_DISBURSAL);
- pendingDisbursalAccountAssignment.setLedgerIdentifier(PENDING_DISBURSAL_LEDGER_IDENTIFIER);
- accountAssignments.add(pendingDisbursalAccountAssignment);
accountAssignments.add(new AccountAssignment(PROCESSING_FEE_INCOME, PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(ORIGINATION_FEE_INCOME, LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(DISBURSEMENT_FEE_INCOME, DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(INTEREST_INCOME, CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(INTEREST_ACCRUAL, LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER));
- accountAssignments.add(new AccountAssignment(LOANS_PAYABLE, LOANS_PAYABLE_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(LATE_FEE_INCOME, LATE_FEE_INCOME_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(LATE_FEE_ACCRUAL, LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(ARREARS_ALLOWANCE, ARREARS_ALLOWANCE_ACCOUNT_IDENTIFIER));
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 50724ed..def64ad 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -35,6 +35,7 @@
import io.mifos.rhythm.spi.v1.client.BeatListener;
import io.mifos.rhythm.spi.v1.domain.BeatPublish;
import io.mifos.rhythm.spi.v1.events.BeatPublishEvent;
+import org.assertj.core.util.Sets;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -67,8 +68,6 @@
private Product product = null;
private Case customerCase = null;
private TaskDefinition taskDefinition = null;
- private CaseParameters caseParameters = null;
- private String pendingDisbursalAccountIdentifier = null;
private String customerLoanAccountIdentifier = null;
private BigDecimal expectedCurrentBalance = null;
@@ -181,16 +180,20 @@
week++;
}
- final BigDecimal minPayment = repayments.stream().min(BigDecimal::compareTo).orElseThrow(IllegalStateException::new);
- final BigDecimal maxPayment = repayments.stream().max(BigDecimal::compareTo).orElseThrow(IllegalStateException::new);
- final BigDecimal delta = maxPayment.subtract(minPayment).abs();
- Assert.assertTrue("Payments are " + repayments,
- delta.divide(maxPayment, BigDecimal.ROUND_HALF_EVEN).compareTo(BigDecimal.valueOf(0.01)) <= 0);
+ checkRepaymentVariance(repayments);
step8Close();
}
+ private void checkRepaymentVariance(List<BigDecimal> repayments) {
+ final BigDecimal minPayment = repayments.stream().min(BigDecimal::compareTo).orElseThrow(IllegalStateException::new);
+ final BigDecimal maxPayment = repayments.stream().max(BigDecimal::compareTo).orElseThrow(IllegalStateException::new);
+ final BigDecimal delta = maxPayment.subtract(minPayment).abs();
+ Assert.assertTrue("Payments vary too much. Payments are " + repayments,
+ delta.divide(maxPayment, BigDecimal.ROUND_HALF_EVEN).compareTo(BigDecimal.valueOf(0.01)) <= 0);
+ }
+
@Test
public void workflowWithOneLateRepayment() throws InterruptedException {
final LocalDateTime today = midnightToday();
@@ -209,7 +212,7 @@
while (expectedCurrentBalance.compareTo(BigDecimal.ZERO) > 0) {
logger.info("Simulating week {}. Expected current balance {}.", week, expectedCurrentBalance);
if (week == weekOfLateRepayment) {
- final BigDecimal lateFee = BigDecimal.valueOf(14_49, MINOR_CURRENCY_UNIT_DIGITS);
+ final BigDecimal lateFee = BigDecimal.valueOf(17_31, MINOR_CURRENCY_UNIT_DIGITS);
step6CalculateInterestAndCheckForLatenessForRangeOfDays(
today,
(week * 7) + 1,
@@ -231,12 +234,7 @@
repayments.remove(3);
- final BigDecimal minPayment = repayments.stream().min(BigDecimal::compareTo).orElseThrow(IllegalStateException::new);
- final BigDecimal maxPayment = repayments.stream().max(BigDecimal::compareTo).orElseThrow(IllegalStateException::new);
- final BigDecimal delta = maxPayment.subtract(minPayment).abs();
- Assert.assertTrue("Payments are " + repayments,
- delta.divide(maxPayment, BigDecimal.ROUND_HALF_EVEN).compareTo(BigDecimal.valueOf(0.01)) <= 0);
-
+ checkRepaymentVariance(repayments);
step8Close();
}
@@ -244,16 +242,16 @@
private BigDecimal findNextRepaymentAmount(
final LocalDateTime referenceDate,
final int dayNumber) {
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
+ AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
- final List<CostComponent> costComponentsForNextPayment = portfolioManager.getCostComponentsForAction(
+ final Payment nextPayment = portfolioManager.getCostComponentsForAction(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.ACCEPT_PAYMENT.name(),
null,
null,
DateConverter.toIsoString(referenceDate.plusDays(dayNumber)));
- return costComponentsForNextPayment.stream().filter(x -> x.getChargeIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID)).findFirst()
+ return nextPayment.getCostComponents().stream().filter(x -> x.getChargeIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID)).findFirst()
.orElseThrow(() -> new IllegalArgumentException("return missing repayment charge."))
.getAmount();
}
@@ -277,7 +275,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.getValue());
+ lowerRangeDisbursementFeeChargeDefinition.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
lowerRangeDisbursementFeeChargeDefinition.setForSegmentSet(DISBURSEMENT_RANGES);
lowerRangeDisbursementFeeChargeDefinition.setFromSegment(DISBURSEMENT_LOWER_RANGE);
lowerRangeDisbursementFeeChargeDefinition.setToSegment(DISBURSEMENT_LOWER_RANGE);
@@ -297,7 +295,7 @@
upperRangeDisbursementFeeChargeDefinition.setChargeAction(lowerRangeDisbursementFeeChargeDefinition.getChargeAction());
upperRangeDisbursementFeeChargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
upperRangeDisbursementFeeChargeDefinition.setAmount(DISBURSEMENT_FEE_UPPER_RANGE_AMOUNT);
- upperRangeDisbursementFeeChargeDefinition.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
+ upperRangeDisbursementFeeChargeDefinition.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
upperRangeDisbursementFeeChargeDefinition.setForSegmentSet(DISBURSEMENT_RANGES);
upperRangeDisbursementFeeChargeDefinition.setFromSegment(DISBURSEMENT_UPPER_RANGE);
upperRangeDisbursementFeeChargeDefinition.setToSegment(DISBURSEMENT_UPPER_RANGE);
@@ -314,8 +312,8 @@
private void step2CreateCase() throws InterruptedException {
logger.info("step2CreateCase");
- caseParameters = Fixture.createAdjustedCaseParameters(x ->
- x.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, null, null, null))
+ final CaseParameters caseParameters = Fixture.createAdjustedCaseParameters(x ->
+ x.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, null, null, null))
);
final String caseParametersAsString = new Gson().toJson(caseParameters);
customerCase = createAdjustedCase(product.getIdentifier(), x -> x.setParameters(caseParametersAsString));
@@ -330,9 +328,8 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.OPEN,
- Collections.singleton(AccountDesignators.ENTRY),
null,
- new CostComponent(ChargeIdentifiers.PROCESSING_FEE_ID, PROCESSING_FEE_AMOUNT));
+ null);
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
@@ -341,11 +338,6 @@
IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE,
Case.State.PENDING);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Action.DENY);
-
- AccountingFixture.verifyTransfer(ledgerManager,
- AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, AccountingFixture.PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER,
- PROCESSING_FEE_AMOUNT, product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN
- );
}
@@ -356,7 +348,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DENY,
- Collections.singleton(AccountDesignators.ENTRY),
+ null,
null);
checkStateTransfer(
product.getIdentifier(),
@@ -379,9 +371,8 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.APPROVE,
- Collections.singleton(AccountDesignators.ENTRY),
null,
- new CostComponent(ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, LOAN_ORIGINATION_FEE_AMOUNT));
+ null);
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
@@ -391,20 +382,9 @@
Case.State.APPROVED);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Action.CLOSE);
- pendingDisbursalAccountIdentifier =
- AccountingFixture.verifyAccountCreation(ledgerManager, AccountingFixture.PENDING_DISBURSAL_LEDGER_IDENTIFIER, AccountType.ASSET);
customerLoanAccountIdentifier =
AccountingFixture.verifyAccountCreation(ledgerManager, AccountingFixture.CUSTOMER_LOAN_LEDGER_IDENTIFIER, AccountType.ASSET);
- final Set<Debtor> debtors = new HashSet<>();
- debtors.add(new Debtor(AccountingFixture.LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER, caseParameters.getMaximumBalance().toPlainString()));
- debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, LOAN_ORIGINATION_FEE_AMOUNT.toPlainString()));
-
- final Set<Creditor> creditors = new HashSet<>();
- creditors.add(new Creditor(pendingDisbursalAccountIdentifier, caseParameters.getMaximumBalance().toPlainString()));
- creditors.add(new Creditor(AccountingFixture.LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER, LOAN_ORIGINATION_FEE_AMOUNT.toPlainString()));
- AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE);
-
expectedCurrentBalance = BigDecimal.ZERO;
}
@@ -418,8 +398,10 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE,
- Collections.singleton(AccountDesignators.ENTRY),
+ Sets.newLinkedHashSet(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN),
amount, new CostComponent(whichDisbursementFee, disbursementFeeAmount),
+ new CostComponent(ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, LOAN_ORIGINATION_FEE_AMOUNT),
+ new CostComponent(ChargeIdentifiers.PROCESSING_FEE_ID, PROCESSING_FEE_AMOUNT),
new CostComponent(ChargeIdentifiers.DISBURSE_PAYMENT_ID, amount));
checkStateTransfer(
product.getIdentifier(),
@@ -434,16 +416,17 @@
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPLY_INTEREST,
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
-
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, disbursementFeeAmount.toPlainString()));
+ debtors.add(new Debtor(customerLoanAccountIdentifier, amount.toPlainString()));
+ debtors.add(new Debtor(customerLoanAccountIdentifier, PROCESSING_FEE_AMOUNT.toPlainString()));
+ debtors.add(new Debtor(customerLoanAccountIdentifier, disbursementFeeAmount.toPlainString()));
+ debtors.add(new Debtor(customerLoanAccountIdentifier, LOAN_ORIGINATION_FEE_AMOUNT.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.TELLER_ONE_ACCOUNT_IDENTIFIER, 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()));
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE);
expectedCurrentBalance = expectedCurrentBalance.add(amount);
@@ -499,7 +482,7 @@
final String beatIdentifier = "alignment0";
final String midnightTimeStamp = DateConverter.toIsoString(forTime);
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
+ AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
final BigDecimal calculatedInterest = expectedCurrentBalance.multiply(Fixture.INTEREST_RATE.divide(Fixture.ACCRUAL_PERIODS, 8, BigDecimal.ROUND_HALF_EVEN))
.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
@@ -509,7 +492,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.APPLY_INTEREST,
- Collections.singleton(AccountDesignators.CUSTOMER_LOAN),
+ null,
null,
new CostComponent(ChargeIdentifiers.INTEREST_ID, calculatedInterest));
@@ -518,7 +501,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.MARK_LATE,
- Collections.singleton(AccountDesignators.CUSTOMER_LOAN),
+ null,
null,
new CostComponent(ChargeIdentifiers.LATE_FEE_ID, calculatedLateFee));
}
@@ -561,9 +544,7 @@
final BigDecimal lateFee) throws InterruptedException {
logger.info("step7PaybackPartialAmount '{}'", amount);
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
-
- final BigDecimal principal = amount.subtract(interestAccrued).subtract(lateFee);
+ AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
checkCostComponentForActionCorrect(
product.getIdentifier(),
@@ -572,7 +553,6 @@
new HashSet<>(Arrays.asList(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN, AccountDesignators.LOAN_FUNDS_SOURCE)),
amount,
new CostComponent(ChargeIdentifiers.REPAYMENT_ID, amount),
- new CostComponent(ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID, principal),
new CostComponent(ChargeIdentifiers.INTEREST_ID, interestAccrued),
new CostComponent(ChargeIdentifiers.LATE_FEE_ID, lateFee));
checkStateTransfer(
@@ -589,16 +569,14 @@
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
final Set<Debtor> debtors = new HashSet<>();
- debtors.add(new Debtor(customerLoanAccountIdentifier, amount.toPlainString()));
- debtors.add(new Debtor(AccountingFixture.LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER, principal.toPlainString()));
+ debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, amount.toPlainString()));
if (interestAccrued.compareTo(BigDecimal.ZERO) != 0)
debtors.add(new Debtor(AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
if (lateFee.compareTo(BigDecimal.ZERO) != 0)
debtors.add(new Debtor(AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER, lateFee.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
- creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, amount.toPlainString()));
- creditors.add(new Creditor(AccountingFixture.LOANS_PAYABLE_ACCOUNT_IDENTIFIER, principal.toPlainString()));
+ creditors.add(new Creditor(customerLoanAccountIdentifier, amount.toPlainString()));
if (interestAccrued.compareTo(BigDecimal.ZERO) != 0)
creditors.add(new Creditor(AccountingFixture.CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
if (lateFee.compareTo(BigDecimal.ZERO) != 0)
@@ -613,13 +591,13 @@
private void step8Close() throws InterruptedException {
logger.info("step8Close");
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
+ AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.CLOSE,
- Collections.singleton(AccountDesignators.ENTRY),
+ null,
null);
checkStateTransfer(
product.getIdentifier(),
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
index 685c183..ec4540d 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
@@ -57,11 +57,7 @@
final Set<String> expectedReadOnlyChargeDefinitionIdentifiers = Stream.of(
ChargeIdentifiers.ALLOW_FOR_WRITE_OFF_ID,
- ChargeIdentifiers.LOAN_FUNDS_ALLOCATION_ID,
- ChargeIdentifiers.RETURN_DISBURSEMENT_ID,
ChargeIdentifiers.DISBURSE_PAYMENT_ID,
- ChargeIdentifiers.TRACK_DISBURSAL_PAYMENT_ID,
- ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID,
ChargeIdentifiers.INTEREST_ID,
ChargeIdentifiers.REPAYMENT_ID)
.collect(Collectors.toSet());
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
index e3520ad..541f490 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
@@ -15,18 +15,15 @@
*/
package io.mifos.portfolio;
-import io.mifos.accounting.api.v1.domain.AccountEntry;
-import io.mifos.core.lang.DateConverter;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.Product;
import org.junit.Test;
-import org.mockito.Matchers;
-import org.mockito.Mockito;
+import java.math.BigDecimal;
+import java.time.Clock;
import java.time.LocalDateTime;
import java.util.Collections;
-import java.util.stream.Stream;
import static io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants.*;
@@ -34,81 +31,9 @@
* @author Myrle Krantz
*/
public class TestCommands extends AbstractPortfolioTest {
- @Test
- public void testHappyWorkflow() throws InterruptedException {
- final Product product = createAndEnableProduct();
- final Case customerCase = createCase(product.getIdentifier());
-
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN);
-
-
- checkStateTransfer(
- product.getIdentifier(),
- customerCase.getIdentifier(),
- Action.OPEN,
- Collections.singletonList(assignEntryToTeller()),
- OPEN_INDIVIDUALLOAN_CASE,
- Case.State.PENDING);
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Action.DENY);
-
-
- checkStateTransfer(product.getIdentifier(),
- customerCase.getIdentifier(),
- Action.APPROVE,
- Collections.singletonList(assignEntryToTeller()),
- APPROVE_INDIVIDUALLOAN_CASE,
- Case.State.APPROVED);
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Action.CLOSE);
-
-
- checkStateTransfer(
- product.getIdentifier(),
- customerCase.getIdentifier(),
- Action.DISBURSE,
- Collections.singletonList(assignEntryToTeller()),
- DISBURSE_INDIVIDUALLOAN_CASE,
- Case.State.ACTIVE);
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
- Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
-
- final AccountEntry firstEntry = new AccountEntry();
- firstEntry.setAmount(2000.0);
- firstEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now()));
- Mockito.doAnswer((x) -> Stream.of(firstEntry))
- .when(ledgerManager)
- .fetchAccountEntriesStream(Matchers.anyString(), Matchers.anyString(), Matchers.anyString(), Matchers.eq("ASC"));
-
-
- checkStateTransfer(
- product.getIdentifier(),
- customerCase.getIdentifier(),
- Action.ACCEPT_PAYMENT,
- Collections.singletonList(assignEntryToTeller()),
- ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
- Case.State.ACTIVE);
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
- Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
-
-
- checkStateTransfer(
- product.getIdentifier(),
- customerCase.getIdentifier(),
- Action.ACCEPT_PAYMENT,
- Collections.singletonList(assignEntryToTeller()),
- ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
- Case.State.ACTIVE);
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
- Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
-
- checkStateTransfer(
- product.getIdentifier(),
- customerCase.getIdentifier(),
- Action.CLOSE,
- Collections.singletonList(assignEntryToTeller()),
- CLOSE_INDIVIDUALLOAN_CASE,
- Case.State.CLOSED);
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
- }
+ // Happy case test deleted because the case is covered in more detail in
+ // TestAccountingInteractionInLoanWorkflow.
+ //public void testHappyWorkflow() throws InterruptedException
@Test
public void testBadCustomerWorkflow() throws InterruptedException {
@@ -142,8 +67,11 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE,
+ LocalDateTime.now(Clock.systemUTC()),
Collections.singletonList(assignEntryToTeller()),
+ BigDecimal.valueOf(2000L),
DISBURSE_INDIVIDUALLOAN_CASE,
+ midnightToday(),
Case.State.ACTIVE);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java b/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java
index 0ef6c65..1f8c03e 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java
@@ -16,6 +16,7 @@
package io.mifos.portfolio;
import com.google.gson.Gson;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.CasePage;
import io.mifos.portfolio.api.v1.domain.Product;
@@ -88,8 +89,8 @@
Assert.assertNotNull(paymentScheduleFirstPage);
paymentScheduleFirstPage.getElements().forEach(x -> {
- x.getCostComponents().forEach(y -> Assert.assertEquals(product.getMinorCurrencyUnitDigits(), y.getAmount().scale()));
- Assert.assertEquals(product.getMinorCurrencyUnitDigits(), x.getRemainingPrincipal().scale());
+ x.getPayment().getCostComponents().forEach(y -> Assert.assertEquals(product.getMinorCurrencyUnitDigits(), y.getAmount().scale()));
+ Assert.assertEquals(product.getMinorCurrencyUnitDigits(), x.getBalances().get(AccountDesignators.CUSTOMER_LOAN).scale());
});
}
diff --git a/service/build.gradle b/service/build.gradle
index 8194a22..4274445 100644
--- a/service/build.gradle
+++ b/service/build.gradle
@@ -47,7 +47,8 @@
[group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator],
[group: 'org.javamoney.lib', name: 'javamoney-calc', version: versions.javamoneylib],
[group: 'javax.money', name: 'money-api', version: '1.0.1'],
- [group: 'org.javamoney', name: 'moneta', version: '1.0.1']
+ [group: 'org.javamoney', name: 'moneta', version: '1.0.1'],
+ [group: 'net.jodah', name: 'expiringmap', version: versions.expiringmap],
)
}
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index 31ea45d..bd76bb4 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -15,7 +15,6 @@
*/
package io.mifos.individuallending;
-import com.google.common.collect.Sets;
import com.google.gson.Gson;
import io.mifos.core.lang.ServiceException;
import io.mifos.customer.api.v1.client.CustomerManager;
@@ -30,10 +29,8 @@
import io.mifos.individuallending.internal.service.CostComponentService;
import io.mifos.individuallending.internal.service.DataContextOfAction;
import io.mifos.individuallending.internal.service.DataContextService;
-import io.mifos.portfolio.api.v1.domain.Case;
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
-import io.mifos.portfolio.api.v1.domain.CostComponent;
-import io.mifos.portfolio.api.v1.domain.Pattern;
+import io.mifos.individuallending.internal.service.PaymentBuilder;
+import io.mifos.portfolio.api.v1.domain.*;
import io.mifos.portfolio.service.ServiceConstants;
import io.mifos.products.spi.PatternFactory;
import io.mifos.products.spi.ProductCommandDispatcher;
@@ -47,7 +44,6 @@
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.*;
import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
@@ -88,8 +84,8 @@
final Set<String> individualLendingRequiredAccounts = new HashSet<>();
individualLendingRequiredAccounts.add(CUSTOMER_LOAN);
- individualLendingRequiredAccounts.add(PENDING_DISBURSAL);
- individualLendingRequiredAccounts.add(LOAN_FUNDS_SOURCE);
+ //TODO: fix in migration individualLendingRequiredAccounts.add(PENDING_DISBURSAL);
+ //was String PENDING_DISBURSAL = "pending-disbursal";
individualLendingRequiredAccounts.add(LOAN_FUNDS_SOURCE);
individualLendingRequiredAccounts.add(PROCESSING_FEE_INCOME);
individualLendingRequiredAccounts.add(ORIGINATION_FEE_INCOME);
@@ -111,35 +107,37 @@
public static List<ChargeDefinition> defaultIndividualLoanCharges() {
final List<ChargeDefinition> ret = new ArrayList<>();
final ChargeDefinition processingFee = charge(
- PROCESSING_FEE_NAME,
- Action.OPEN,
- BigDecimal.ONE,
- ENTRY,
- PROCESSING_FEE_INCOME);
+ PROCESSING_FEE_NAME,
+ Action.DISBURSE, //TODO: fix existing charges in migration
+ BigDecimal.ONE,
+ CUSTOMER_LOAN, //TODO: fix existing charges in migration
+ PROCESSING_FEE_INCOME);
processingFee.setReadOnly(false);
final ChargeDefinition loanOriginationFee = charge(
- LOAN_ORIGINATION_FEE_NAME,
- Action.APPROVE,
- BigDecimal.ONE,
- ENTRY,
- ORIGINATION_FEE_INCOME);
+ LOAN_ORIGINATION_FEE_NAME,
+ Action.DISBURSE, //TODO: fix existing charges in migration
+ BigDecimal.ONE,
+ CUSTOMER_LOAN, //TODO: fix existing charges in migration
+ ORIGINATION_FEE_INCOME);
loanOriginationFee.setReadOnly(false);
- final ChargeDefinition loanFundsAllocation = charge(
+ /*final ChargeDefinition loanFundsAllocation = charge(
LOAN_FUNDS_ALLOCATION_ID,
Action.APPROVE,
BigDecimal.valueOf(100),
LOAN_FUNDS_SOURCE,
PENDING_DISBURSAL);
- loanFundsAllocation.setReadOnly(true);
+ loanFundsAllocation.setReadOnly(true);*/
+ //TODO: handle removing this extraneous charge in migration.
final ChargeDefinition disbursementFee = charge(
- DISBURSEMENT_FEE_NAME,
- Action.DISBURSE,
- BigDecimal.valueOf(0.1),
- ENTRY,
- DISBURSEMENT_FEE_INCOME);
+ DISBURSEMENT_FEE_NAME,
+ Action.DISBURSE,
+ BigDecimal.valueOf(0.1),
+ CUSTOMER_LOAN, //TODO: fix existing charges in migration
+ DISBURSEMENT_FEE_INCOME);
+ disbursementFee.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue()); //TODO: fix existing charges in migration
disbursementFee.setReadOnly(false);
final ChargeDefinition disbursePayment = new ChargeDefinition();
@@ -147,13 +145,14 @@
disbursePayment.setIdentifier(DISBURSE_PAYMENT_ID);
disbursePayment.setName(DISBURSE_PAYMENT_NAME);
disbursePayment.setDescription(DISBURSE_PAYMENT_NAME);
- disbursePayment.setFromAccountDesignator(LOANS_PAYABLE);
+ disbursePayment.setFromAccountDesignator(CUSTOMER_LOAN); //TODO: fix existing charges in migration
disbursePayment.setToAccountDesignator(ENTRY);
- disbursePayment.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
+ disbursePayment.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
disbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
disbursePayment.setAmount(BigDecimal.valueOf(100));
disbursePayment.setReadOnly(true);
+ /*
final ChargeDefinition trackPrincipalDisbursePayment = new ChargeDefinition();
trackPrincipalDisbursePayment.setChargeAction(Action.DISBURSE.name());
trackPrincipalDisbursePayment.setIdentifier(TRACK_DISBURSAL_PAYMENT_ID);
@@ -161,10 +160,11 @@
trackPrincipalDisbursePayment.setDescription(TRACK_DISBURSAL_PAYMENT_NAME);
trackPrincipalDisbursePayment.setFromAccountDesignator(PENDING_DISBURSAL);
trackPrincipalDisbursePayment.setToAccountDesignator(CUSTOMER_LOAN);
- trackPrincipalDisbursePayment.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
+ trackPrincipalDisbursePayment.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
trackPrincipalDisbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
trackPrincipalDisbursePayment.setAmount(BigDecimal.valueOf(100));
- trackPrincipalDisbursePayment.setReadOnly(true);
+ trackPrincipalDisbursePayment.setReadOnly(true);*/
+ //TODO: handle removing this extraneous charge in migration.
final ChargeDefinition lateFee = charge(
LATE_FEE_NAME,
@@ -174,17 +174,17 @@
LATE_FEE_INCOME);
lateFee.setAccrueAction(Action.MARK_LATE.name());
lateFee.setAccrualAccountDesignator(LATE_FEE_ACCRUAL);
- lateFee.setProportionalTo(ChargeProportionalDesignator.REPAYMENT_DESIGNATOR.getValue());
+ lateFee.setProportionalTo(ChargeProportionalDesignator.CONTRACTUAL_REPAYMENT_DESIGNATOR.getValue());
lateFee.setChargeOnTop(true);
lateFee.setReadOnly(false);
//TODO: Make multiple write off allowance charges.
final ChargeDefinition writeOffAllowanceCharge = charge(
- ALLOW_FOR_WRITE_OFF_NAME,
- Action.MARK_LATE,
- BigDecimal.valueOf(30),
- PENDING_DISBURSAL,
- ARREARS_ALLOWANCE);
+ ALLOW_FOR_WRITE_OFF_NAME,
+ Action.MARK_LATE,
+ BigDecimal.valueOf(30),
+ LOAN_FUNDS_SOURCE, //TODO: this and previous value ("pending-disbursal") are not correct and will require migration.
+ ARREARS_ALLOWANCE);
writeOffAllowanceCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
writeOffAllowanceCharge.setReadOnly(true);
@@ -206,46 +206,48 @@
customerRepaymentCharge.setIdentifier(REPAYMENT_ID);
customerRepaymentCharge.setName(REPAYMENT_NAME);
customerRepaymentCharge.setDescription(REPAYMENT_NAME);
- customerRepaymentCharge.setFromAccountDesignator(CUSTOMER_LOAN);
- customerRepaymentCharge.setToAccountDesignator(ENTRY);
- customerRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REPAYMENT_DESIGNATOR.getValue());
+ customerRepaymentCharge.setFromAccountDesignator(ENTRY); //TODO: fix existing charges in migration
+ customerRepaymentCharge.setToAccountDesignator(CUSTOMER_LOAN); //TODO: fix existing charges in migration
+ customerRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REQUESTED_REPAYMENT_DESIGNATOR.getValue());
customerRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
customerRepaymentCharge.setAmount(BigDecimal.valueOf(100));
customerRepaymentCharge.setReadOnly(true);
- final ChargeDefinition trackReturnPrincipalCharge = new ChargeDefinition();
+ /*final ChargeDefinition trackReturnPrincipalCharge = new ChargeDefinition();
trackReturnPrincipalCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
trackReturnPrincipalCharge.setIdentifier(TRACK_RETURN_PRINCIPAL_ID);
trackReturnPrincipalCharge.setName(TRACK_RETURN_PRINCIPAL_NAME);
trackReturnPrincipalCharge.setDescription(TRACK_RETURN_PRINCIPAL_NAME);
trackReturnPrincipalCharge.setFromAccountDesignator(LOAN_FUNDS_SOURCE);
trackReturnPrincipalCharge.setToAccountDesignator(LOANS_PAYABLE);
- trackReturnPrincipalCharge.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
+ trackReturnPrincipalCharge.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
trackReturnPrincipalCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
trackReturnPrincipalCharge.setAmount(BigDecimal.valueOf(100));
- trackReturnPrincipalCharge.setReadOnly(true);
+ trackReturnPrincipalCharge.setReadOnly(true);*/
+ //TODO: handle removing this extraneous charge in migration.
- final ChargeDefinition disbursementReturnCharge = charge(
+ /*final ChargeDefinition disbursementReturnCharge = charge(
RETURN_DISBURSEMENT_NAME,
Action.CLOSE,
BigDecimal.valueOf(100),
PENDING_DISBURSAL,
LOAN_FUNDS_SOURCE);
disbursementReturnCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
- disbursementReturnCharge.setReadOnly(true);
+ disbursementReturnCharge.setReadOnly(true);*/
+ //TODO: handle removing this extraneous charge in migration.
ret.add(processingFee);
ret.add(loanOriginationFee);
- ret.add(loanFundsAllocation);
+ //TODO: ret.add(loanFundsAllocation);
ret.add(disbursementFee);
ret.add(disbursePayment);
- ret.add(trackPrincipalDisbursePayment);
+ //TODO: ret.add(trackPrincipalDisbursePayment);
ret.add(lateFee);
ret.add(writeOffAllowanceCharge);
ret.add(interestCharge);
ret.add(customerRepaymentCharge);
- ret.add(trackReturnPrincipalCharge);
- ret.add(disbursementReturnCharge);
+ //TODO: ret.add(trackReturnPrincipalCharge);
+ //TODO: ret.add(disbursementReturnCharge);
return ret;
}
@@ -344,7 +346,7 @@
}
@Override
- public List<CostComponent> getCostComponentsForAction(
+ public Payment getCostComponentsForAction(
final String productIdentifier,
final String caseIdentifier,
final String actionIdentifier,
@@ -356,35 +358,13 @@
final Case.State caseState = Case.State.valueOf(dataContextOfAction.getCustomerCaseEntity().getCurrentState());
checkActionCanBeExecuted(caseState, action);
- Stream<Map.Entry<ChargeDefinition, CostComponent>> costComponentStream = costComponentService.getCostComponentsForAction(
+ final PaymentBuilder paymentBuilder = costComponentService.getCostComponentsForAction(
action,
dataContextOfAction,
forPaymentSize,
- forDateTime.toLocalDate())
- .stream();
+ forDateTime.toLocalDate());
- if (!forAccountDesignators.isEmpty()) {
- costComponentStream = costComponentStream
- .filter(costComponentEntry -> chargeReferencesAccountDesignators(costComponentEntry.getKey(), action, forAccountDesignators));
- }
-
- return costComponentStream
- .map(costComponentEntry -> new CostComponent(costComponentEntry.getKey().getIdentifier(), costComponentEntry.getValue().getAmount()))
- .collect(Collectors.toList());
- }
-
- private boolean chargeReferencesAccountDesignators(
- final ChargeDefinition chargeDefinition,
- final Action action,
- final Set<String> forAccountDesignators) {
- final Set<String> accountsToCompare = Sets.newHashSet(
- chargeDefinition.getFromAccountDesignator(),
- chargeDefinition.getToAccountDesignator()
- );
- if (chargeDefinition.getAccrualAccountDesignator() != null)
- accountsToCompare.add(chargeDefinition.getAccrualAccountDesignator());
-
- return !Sets.intersection(accountsToCompare, forAccountDesignators).isEmpty();
+ return paymentBuilder.buildPayment(action, forAccountDesignators);
}
public static void checkActionCanBeExecuted(final Case.State state, final Action action) {
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 d6e6aed..fb51ef3 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
@@ -32,7 +32,6 @@
import io.mifos.individuallending.internal.service.*;
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.Case;
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.CostComponent;
import io.mifos.portfolio.api.v1.events.EventConstants;
import io.mifos.portfolio.service.internal.mapper.CaseMapper;
@@ -99,20 +98,13 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.OPEN);
- final CostComponentsForRepaymentPeriod costComponents
+ final PaymentBuilder paymentBuilder
= costComponentService.getCostComponentsForOpen(dataContextOfAction);
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final List<ChargeInstance> charges = costComponents.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.OPEN,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.OPEN, designatorToAccountIdentifierMapper);
final LocalDateTime today = today();
@@ -143,20 +135,13 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.DENY);
- final CostComponentsForRepaymentPeriod costComponents
+ final PaymentBuilder paymentBuilder
= costComponentService.getCostComponentsForDeny(dataContextOfAction);
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final List<ChargeInstance> charges = costComponents.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.DENY,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.DENY, designatorToAccountIdentifierMapper);
final LocalDateTime today = today();
@@ -195,17 +180,10 @@
);
caseRepository.save(dataContextOfAction.getCustomerCaseEntity());
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ final PaymentBuilder paymentBuilder =
costComponentService.getCostComponentsForApprove(dataContextOfAction);
- final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.APPROVE,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.APPROVE, designatorToAccountIdentifierMapper);
final LocalDateTime today = today();
@@ -236,21 +214,13 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.DISBURSE);
final BigDecimal disbursalAmount = Optional.ofNullable(command.getCommand().getPaymentSize()).orElse(BigDecimal.ZERO);
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ final PaymentBuilder paymentBuilder =
costComponentService.getCostComponentsForDisburse(dataContextOfAction, disbursalAmount);
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final List<ChargeInstance> charges =
- costComponentsForRepaymentPeriod.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.DISBURSE,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.DISBURSE, designatorToAccountIdentifierMapper);
final LocalDateTime today = today();
@@ -272,7 +242,7 @@
final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
- final BigDecimal newLoanPaymentSize = costComponentService.getLoanPaymentSize(
+ final BigDecimal newLoanPaymentSize = costComponentService.getLoanPaymentSizeForSingleDisbursement(
currentBalance.add(disbursalAmount),
dataContextOfAction);
@@ -298,20 +268,13 @@
throw ServiceException.internalError(
"End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ final PaymentBuilder paymentBuilder =
costComponentService.getCostComponentsForApplyInterest(dataContextOfAction);
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.APPLY_INTEREST,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.APPLY_INTEREST, designatorToAccountIdentifierMapper);
accountingAdapter.bookCharges(charges,
"Applied interest on " + command.getForTime(),
@@ -340,7 +303,7 @@
throw ServiceException.internalError(
"End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ final PaymentBuilder paymentBuilder =
costComponentService.getCostComponentsForAcceptPayment(
dataContextOfAction,
command.getCommand().getPaymentSize(),
@@ -349,14 +312,7 @@
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.ACCEPT_PAYMENT,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.ACCEPT_PAYMENT, designatorToAccountIdentifierMapper);
final LocalDateTime today = today();
@@ -387,20 +343,13 @@
throw ServiceException.internalError(
"End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ final PaymentBuilder paymentBuilder =
costComponentService.getCostComponentsForMarkLate(dataContextOfAction, DateConverter.fromIsoString(command.getForTime()));
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.MARK_LATE,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.MARK_LATE, designatorToAccountIdentifierMapper);
final LocalDateTime today = today();
@@ -446,29 +395,21 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.CLOSE);
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ final PaymentBuilder paymentBuilder =
costComponentService.getCostComponentsForClose(dataContextOfAction);
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final List<ChargeInstance> charges =
- costComponentsForRepaymentPeriod.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.DISBURSE,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.CLOSE, designatorToAccountIdentifierMapper);
final LocalDateTime today = today();
accountingAdapter.bookCharges(charges,
command.getCommand().getNote(),
command.getCommand().getCreatedOn(),
- dataContextOfAction.getMessageForCharge(Action.DISBURSE),
- Action.DISBURSE.getTransactionType());
+ dataContextOfAction.getMessageForCharge(Action.CLOSE),
+ Action.CLOSE.getTransactionType());
final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
customerCase.setCurrentState(Case.State.CLOSED.name());
@@ -498,36 +439,6 @@
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
}
- private static Optional<ChargeInstance> mapCostComponentEntryToChargeInstance(
- final Action action,
- final Map.Entry<ChargeDefinition, CostComponent> costComponentEntry,
- final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
- final ChargeDefinition chargeDefinition = costComponentEntry.getKey();
- final BigDecimal chargeAmount = costComponentEntry.getValue().getAmount();
-
- if (CostComponentService.chargeIsAccrued(chargeDefinition)) {
- if (Action.valueOf(chargeDefinition.getAccrueAction()) == action)
- return Optional.of(new ChargeInstance(
- designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
- designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
- chargeAmount));
- else if (Action.valueOf(chargeDefinition.getChargeAction()) == action)
- return Optional.of(new ChargeInstance(
- designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
- designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
- chargeAmount));
- else
- return Optional.empty();
- }
- else if (Action.valueOf(chargeDefinition.getChargeAction()) == action)
- return Optional.of(new ChargeInstance(
- designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
- designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
- chargeAmount));
- else
- return Optional.empty();
- }
-
private Map<String, BigDecimal> getRequestedChargeAmounts(final @Nullable List<CostComponent> costComponents) {
if (costComponents == null)
return Collections.emptyMap();
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 120378b..cbca5ee 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
@@ -58,7 +58,7 @@
this.accountingAdapter = accountingAdapter;
}
- public CostComponentsForRepaymentPeriod getCostComponentsForAction(
+ public PaymentBuilder getCostComponentsForAction(
final Action action,
final DataContextOfAction dataContextOfAction,
final BigDecimal forPaymentSize,
@@ -89,7 +89,7 @@
}
}
- public CostComponentsForRepaymentPeriod getCostComponentsForOpen(final DataContextOfAction dataContextOfAction) {
+ public PaymentBuilder getCostComponentsForOpen(final DataContextOfAction dataContextOfAction) {
final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
@@ -101,6 +101,8 @@
Collections.emptyMap(),
scheduledCharges,
caseParameters.getBalanceRangeMaximum(),
+ new SimulatedRunningBalances(),
+ BigDecimal.ZERO,
BigDecimal.ZERO,
BigDecimal.ZERO,
dataContextOfAction.getInterest(),
@@ -108,7 +110,7 @@
true);
}
- public CostComponentsForRepaymentPeriod getCostComponentsForDeny(final DataContextOfAction dataContextOfAction) {
+ public PaymentBuilder getCostComponentsForDeny(final DataContextOfAction dataContextOfAction) {
final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
@@ -120,6 +122,8 @@
Collections.emptyMap(),
scheduledCharges,
caseParameters.getBalanceRangeMaximum(),
+ new SimulatedRunningBalances(),
+ BigDecimal.ZERO,
BigDecimal.ZERO,
BigDecimal.ZERO,
dataContextOfAction.getInterest(),
@@ -127,7 +131,7 @@
true);
}
- public CostComponentsForRepaymentPeriod getCostComponentsForApprove(final DataContextOfAction dataContextOfAction) {
+ public PaymentBuilder getCostComponentsForApprove(final DataContextOfAction dataContextOfAction) {
//Charge the approval fee if applicable.
final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
@@ -140,6 +144,8 @@
Collections.emptyMap(),
scheduledCharges,
caseParameters.getBalanceRangeMaximum(),
+ new SimulatedRunningBalances(),
+ BigDecimal.ZERO,
BigDecimal.ZERO,
BigDecimal.ZERO,
dataContextOfAction.getInterest(),
@@ -147,13 +153,14 @@
true);
}
- public CostComponentsForRepaymentPeriod getCostComponentsForDisburse(
+ public PaymentBuilder getCostComponentsForDisburse(
final @Nonnull DataContextOfAction dataContextOfAction,
final @Nullable BigDecimal requestedDisbursalSize) {
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
- final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
+ final RealRunningBalances runningBalances = new RealRunningBalances(accountingAdapter, designatorToAccountIdentifierMapper);
+ final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN);
if (requestedDisbursalSize != null &&
dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum().compareTo(
@@ -170,9 +177,9 @@
final BigDecimal disbursalSize;
if (requestedDisbursalSize == null)
- disbursalSize = dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum().negate();
+ disbursalSize = dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum();
else
- disbursalSize = requestedDisbursalSize.negate();
+ disbursalSize = requestedDisbursalSize;
final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier, scheduledActions);
@@ -197,20 +204,22 @@
accruedCostComponents,
chargesSplitIntoScheduledAndAccrued.get(false),
caseParameters.getBalanceRangeMaximum(),
- currentBalance,
+ runningBalances,
+ dataContextOfAction.getCaseParametersEntity().getPaymentSize(),
disbursalSize,
+ BigDecimal.ZERO,
dataContextOfAction.getInterest(),
minorCurrencyUnitDigits,
true);
}
- public CostComponentsForRepaymentPeriod getCostComponentsForApplyInterest(
+ public PaymentBuilder getCostComponentsForApplyInterest(
final DataContextOfAction dataContextOfAction)
{
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
- final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
+ final RunningBalances runningBalances = new RealRunningBalances(accountingAdapter, designatorToAccountIdentifierMapper);
final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
@@ -237,14 +246,16 @@
accruedCostComponents,
chargesSplitIntoScheduledAndAccrued.get(false),
caseParameters.getBalanceRangeMaximum(),
- currentBalance,
+ runningBalances,
+ dataContextOfAction.getCaseParametersEntity().getPaymentSize(),
+ BigDecimal.ZERO,
BigDecimal.ZERO,
dataContextOfAction.getInterest(),
minorCurrencyUnitDigits,
true);
}
- public CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(
+ public PaymentBuilder getCostComponentsForAcceptPayment(
final DataContextOfAction dataContextOfAction,
final @Nullable BigDecimal requestedLoanPaymentSize,
final LocalDate forDate)
@@ -252,7 +263,7 @@
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
- final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
+ final RealRunningBalances runningBalances = new RealRunningBalances(accountingAdapter, designatorToAccountIdentifierMapper);
final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
@@ -288,10 +299,11 @@
}
else {
if (scheduledAction.actionPeriod != null && scheduledAction.actionPeriod.isLastPeriod()) {
- loanPaymentSize = currentBalance;
+ loanPaymentSize = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN);
}
else {
- final BigDecimal paymentSizeBeforeOnTopCharges = currentBalance.min(dataContextOfAction.getCaseParametersEntity().getPaymentSize());
+ final BigDecimal paymentSizeBeforeOnTopCharges = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN)
+ .min(dataContextOfAction.getCaseParametersEntity().getPaymentSize());
@SuppressWarnings("UnnecessaryLocalVariable")
final BigDecimal paymentSizeIncludingOnTopCharges = accruedCostComponents.entrySet().stream()
@@ -308,21 +320,25 @@
accruedCostComponents,
chargesSplitIntoScheduledAndAccrued.get(false),
caseParameters.getBalanceRangeMaximum(),
- currentBalance,
+ runningBalances,
+ dataContextOfAction.getCaseParametersEntity().getPaymentSize(),
+ BigDecimal.ZERO,
loanPaymentSize,
dataContextOfAction.getInterest(),
minorCurrencyUnitDigits,
true);
}
- public CostComponentsForRepaymentPeriod getCostComponentsForClose(final DataContextOfAction dataContextOfAction) {
+ public PaymentBuilder getCostComponentsForClose(final DataContextOfAction dataContextOfAction) {
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
- final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
- if (currentBalance.compareTo(BigDecimal.ZERO) != 0)
+ final RealRunningBalances runningBalances = new RealRunningBalances(accountingAdapter, designatorToAccountIdentifierMapper);
+
+ if (runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN).compareTo(BigDecimal.ZERO) != 0)
throw ServiceException.conflict("Cannot close loan until the balance is zero.");
+
final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
@@ -348,19 +364,23 @@
accruedCostComponents,
chargesSplitIntoScheduledAndAccrued.get(false),
caseParameters.getBalanceRangeMaximum(),
- currentBalance,
+ runningBalances,
+ dataContextOfAction.getCaseParametersEntity().getPaymentSize(),
+ BigDecimal.ZERO,
BigDecimal.ZERO,
dataContextOfAction.getInterest(),
minorCurrencyUnitDigits,
true);
}
- public CostComponentsForRepaymentPeriod getCostComponentsForMarkLate(final DataContextOfAction dataContextOfAction,
- final LocalDateTime forTime) {
+ public PaymentBuilder getCostComponentsForMarkLate(
+ final DataContextOfAction dataContextOfAction,
+ final LocalDateTime forTime)
+ {
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
- final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
+ final RunningBalances runningBalances = new RealRunningBalances(accountingAdapter, designatorToAccountIdentifierMapper);
final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
@@ -389,46 +409,46 @@
accruedCostComponents,
chargesSplitIntoScheduledAndAccrued.get(false),
caseParameters.getBalanceRangeMaximum(),
- currentBalance,
+ runningBalances,
loanPaymentSize,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
dataContextOfAction.getInterest(),
minorCurrencyUnitDigits,
true);
}
- private CostComponentsForRepaymentPeriod getCostComponentsForWriteOff(final DataContextOfAction dataContextOfAction) {
+ private PaymentBuilder getCostComponentsForWriteOff(final DataContextOfAction dataContextOfAction) {
return null;
}
- private CostComponentsForRepaymentPeriod getCostComponentsForRecover(final DataContextOfAction dataContextOfAction) {
+ private PaymentBuilder getCostComponentsForRecover(final DataContextOfAction dataContextOfAction) {
return null;
}
- static CostComponentsForRepaymentPeriod getCostComponentsForScheduledCharges(
+ static PaymentBuilder getCostComponentsForScheduledCharges(
final Map<ChargeDefinition, CostComponent> accruedCostComponents,
final Collection<ScheduledCharge> scheduledCharges,
final BigDecimal maximumBalance,
- final BigDecimal runningBalance,
- final BigDecimal entryAccountAdjustment, //disbursement or payment size.
+ final RunningBalances preChargeBalances,
+ final BigDecimal contractualRepayment,
+ final BigDecimal requestedDisbursement,
+ final BigDecimal requestedRepayment,
final BigDecimal interest,
final int minorCurrencyUnitDigits,
final boolean accrualAccounting) {
- final Map<String, BigDecimal> balanceAdjustments = new HashMap<>();
- balanceAdjustments.put(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO);
-
- final Map<ChargeDefinition, CostComponent> costComponentMap = new HashMap<>();
+ final PaymentBuilder paymentBuilder = new PaymentBuilder(preChargeBalances, accrualAccounting);
+ //TODO: once you've fixed getAmountProportionalTo, use the passed in variable.
for (Map.Entry<ChargeDefinition, CostComponent> entry : accruedCostComponents.entrySet()) {
final ChargeDefinition chargeDefinition = entry.getKey();
final BigDecimal chargeAmount = entry.getValue().getAmount();
- costComponentMap.put(
- chargeDefinition,
- entry.getValue());
//TODO: This should adjust differently depending on accrual accounting.
// It can't be fixed until getAmountProportionalTo is fixed.
- adjustBalance(chargeDefinition.getFromAccountDesignator(), chargeAmount.negate(), balanceAdjustments);
- adjustBalance(chargeDefinition.getToAccountDesignator(), chargeAmount, balanceAdjustments);
+ paymentBuilder.addToBalance(chargeDefinition.getFromAccountDesignator(), chargeAmount.negate());
+ paymentBuilder.addToBalance(chargeDefinition.getToAccountDesignator(), chargeAmount);
+ paymentBuilder.addToCostComponent(chargeDefinition, chargeAmount);
}
@@ -437,84 +457,80 @@
final BigDecimal amountProportionalTo = getAmountProportionalTo(
scheduledCharge,
maximumBalance,
- runningBalance,
- entryAccountAdjustment,
- balanceAdjustments);
+ preChargeBalances,
+ contractualRepayment,
+ requestedDisbursement,
+ requestedRepayment,
+ paymentBuilder);
//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);
-
final BigDecimal chargeAmount = howToApplyScheduledChargeToAmount(scheduledCharge, interest)
.apply(amountProportionalTo)
.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
- adjustBalances(
+ paymentBuilder.adjustBalances(
scheduledCharge.getScheduledAction().action,
scheduledCharge.getChargeDefinition(),
- chargeAmount,
- balanceAdjustments,
- false); //TODO: once you've fixed getAmountProportionalTo, use the passed in variable.
- costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
+ chargeAmount);
}
}
- return new CostComponentsForRepaymentPeriod(
- costComponentMap,
- balanceAdjustments.getOrDefault(AccountDesignators.LOANS_PAYABLE, BigDecimal.ZERO).negate());
+ return paymentBuilder;
}
private static BigDecimal getAmountProportionalTo(
final ScheduledCharge scheduledCharge,
final BigDecimal maximumBalance,
- final BigDecimal runningBalance,
- final BigDecimal loanPaymentSize,
- final Map<String, BigDecimal> balanceAdjustments) {
+ final RunningBalances runningBalances,
+ final BigDecimal contractualRepayment,
+ final BigDecimal requestedDisbursement,
+ final BigDecimal requestedRepayment,
+ final PaymentBuilder paymentBuilder) {
final Optional<ChargeProportionalDesignator> optionalChargeProportionalTo
= ChargeProportionalDesignator.fromString(scheduledCharge.getChargeDefinition().getProportionalTo());
return optionalChargeProportionalTo.map(chargeProportionalTo ->
- getAmountProportionalTo(chargeProportionalTo, maximumBalance, runningBalance, loanPaymentSize, balanceAdjustments))
+ getAmountProportionalTo(
+ chargeProportionalTo,
+ maximumBalance,
+ runningBalances,
+ contractualRepayment,
+ requestedDisbursement,
+ requestedRepayment,
+ paymentBuilder))
.orElse(BigDecimal.ZERO);
}
static BigDecimal getAmountProportionalTo(
final ChargeProportionalDesignator chargeProportionalTo,
final BigDecimal maximumBalance,
- final BigDecimal runningBalance,
- final BigDecimal loanPaymentSize,
- final Map<String, BigDecimal> balanceAdjustments) {
+ final RunningBalances runningBalances,
+ final BigDecimal contractualRepayment,
+ final BigDecimal requestedDisbursement,
+ final BigDecimal requestedRepayment,
+ final PaymentBuilder paymentBuilder) {
switch (chargeProportionalTo) {
case NOT_PROPORTIONAL:
- return BigDecimal.ZERO;
+ return BigDecimal.ONE;
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: {
- if (loanPaymentSize.compareTo(BigDecimal.ZERO) <= 0)
- return loanPaymentSize.abs();
- final BigDecimal newRunningBalance
- = runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
- final BigDecimal newLoanPaymentSize = loanPaymentSize.min(newRunningBalance);
- return newLoanPaymentSize.add(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO)).abs();
+ case RUNNING_BALANCE_DESIGNATOR: {
+ final BigDecimal customerLoanRunningBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN);
+ return customerLoanRunningBalance.subtract(paymentBuilder.getBalanceAdjustment(AccountDesignators.CUSTOMER_LOAN));
}
+ case CONTRACTUAL_REPAYMENT_DESIGNATOR:
+ return contractualRepayment;
+ case REQUESTED_DISBURSEMENT_DESIGNATOR:
+ return requestedDisbursement;
+ case REQUESTED_REPAYMENT_DESIGNATOR:
+ return requestedRepayment;
default:
return BigDecimal.ZERO;
}
//TODO: correctly implement charges which are proportional to other charges.
}
- private static CostComponent constructEmptyCostComponent(final ChargeDefinition chargeDefinition) {
- final CostComponent ret = new CostComponent();
- ret.setChargeIdentifier(chargeDefinition.getIdentifier());
- ret.setAmount(BigDecimal.ZERO);
- return ret;
- }
-
private static Function<BigDecimal, BigDecimal> howToApplyScheduledChargeToAmount(
final ScheduledCharge scheduledCharge, final BigDecimal interest)
{
@@ -537,8 +553,8 @@
}
}
- public BigDecimal getLoanPaymentSize(
- final BigDecimal assumedBalance,
+ public BigDecimal getLoanPaymentSizeForSingleDisbursement(
+ final BigDecimal disbursementSize,
final DataContextOfAction dataContextOfAction) {
final List<ScheduledAction> hypotheticalScheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(
today(),
@@ -547,73 +563,54 @@
dataContextOfAction.getProductEntity().getIdentifier(),
hypotheticalScheduledActions);
return getLoanPaymentSize(
- assumedBalance,
+ disbursementSize,
+ disbursementSize,
dataContextOfAction.getInterest(),
dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits(),
hypotheticalScheduledCharges);
}
- static BigDecimal getLoanPaymentSize(final BigDecimal startingBalance,
- final BigDecimal interest,
- final int minorCurrencyUnitDigits,
- final List<ScheduledCharge> scheduledCharges) {
- final int precision = startingBalance.precision() + minorCurrencyUnitDigits + EXTRA_PRECISION;
+ static BigDecimal getLoanPaymentSize(
+ final BigDecimal maximumBalanceSize,
+ final BigDecimal disbursementSize,
+ final BigDecimal interest,
+ final int minorCurrencyUnitDigits,
+ final List<ScheduledCharge> scheduledCharges) {
+ final int precision = disbursementSize.precision() - 4 + minorCurrencyUnitDigits + EXTRA_PRECISION;
final Map<Period, BigDecimal> accrualRatesByPeriod
- = PeriodChargeCalculator.getPeriodAccrualInterestRate(interest, scheduledCharges, precision);
+ = PeriodChargeCalculator.getPeriodAccrualInterestRate(interest, scheduledCharges, disbursementSize.precision());
final int periodCount = accrualRatesByPeriod.size();
if (periodCount == 0)
- return startingBalance;
+ return disbursementSize;
final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream()
.collect(RateCollectors.geometricMean(precision));
+ final List<ScheduledCharge> disbursementFees = scheduledCharges.stream()
+ .filter(x -> x.getScheduledAction().action.equals(Action.DISBURSE))
+ .collect(Collectors.toList());
+ final PaymentBuilder paymentBuilder = getCostComponentsForScheduledCharges(
+ Collections.emptyMap(),
+ disbursementFees,
+ maximumBalanceSize,
+ new SimulatedRunningBalances(),
+ BigDecimal.ZERO, //Contractual repayment not determined yet here.
+ disbursementSize,
+ BigDecimal.ZERO,
+ interest,
+ minorCurrencyUnitDigits,
+ false
+ );
+ final BigDecimal finalDisbursementSize = paymentBuilder.getBalanceAdjustment(AccountDesignators.CUSTOMER_LOAN).negate();
+
final MonetaryAmount presentValue = AnnuityPayment.calculate(
- Money.of(startingBalance, "XXX"),
+ Money.of(finalDisbursementSize, "XXX"),
Rate.of(geometricMeanAccrualRate),
periodCount);
return BigDecimal.valueOf(presentValue.getNumber().doubleValueExact()).setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
}
- private static void adjustBalances(
- final Action action,
- final ChargeDefinition chargeDefinition,
- final BigDecimal chargeAmount,
- final Map<String, BigDecimal> balanceAdjustments,
- boolean accrualAccounting) {
- if (accrualAccounting) {
- if (chargeIsAccrued(chargeDefinition)) {
- if (Action.valueOf(chargeDefinition.getAccrueAction()) == action) {
- adjustBalance(chargeDefinition.getFromAccountDesignator(), chargeAmount.negate(), balanceAdjustments);
- adjustBalance(chargeDefinition.getAccrualAccountDesignator(), chargeAmount, balanceAdjustments);
- } else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
- adjustBalance(chargeDefinition.getAccrualAccountDesignator(), chargeAmount.negate(), balanceAdjustments);
- adjustBalance(chargeDefinition.getToAccountDesignator(), chargeAmount, balanceAdjustments);
- }
- } else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
- adjustBalance(chargeDefinition.getFromAccountDesignator(), chargeAmount.negate(), balanceAdjustments);
- adjustBalance(chargeDefinition.getToAccountDesignator(), chargeAmount, balanceAdjustments);
- }
- }
- else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
- adjustBalance(chargeDefinition.getFromAccountDesignator(), chargeAmount.negate(), balanceAdjustments);
- adjustBalance(chargeDefinition.getToAccountDesignator(), chargeAmount, balanceAdjustments);
- }
- }
-
- private static void adjustBalance(
- final String designator,
- final BigDecimal chargeAmount,
- final Map<String, BigDecimal> balanceAdjustments) {
- final BigDecimal balance = balanceAdjustments.computeIfAbsent(designator, (x) -> BigDecimal.ZERO);
- final BigDecimal newBalance = balance.add(chargeAmount);
- balanceAdjustments.put(designator, newBalance);
- }
-
- public static boolean chargeIsAccrued(final ChargeDefinition chargeDefinition) {
- return chargeDefinition.getAccrualAccountDesignator() != null;
- }
-
private static boolean isAccruedChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
return chargeDefinition.getAccrueAction() != null &&
chargeDefinition.getChargeAction().equals(action.name());
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
deleted file mode 100644
index 83372f4..0000000
--- a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2017 The Mifos Initiative.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.mifos.individuallending.internal.service;
-
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
-import io.mifos.portfolio.api.v1.domain.CostComponent;
-
-import java.math.BigDecimal;
-import java.util.Map;
-import java.util.stream.Stream;
-
-/**
- * @author Myrle Krantz
- */
-public class CostComponentsForRepaymentPeriod {
- final private Map<ChargeDefinition, CostComponent> costComponents;
- final private BigDecimal balanceAdjustment;
-
- CostComponentsForRepaymentPeriod(
- final Map<ChargeDefinition, CostComponent> costComponents,
- final BigDecimal balanceAdjustment) {
- this.costComponents = costComponents;
- this.balanceAdjustment = balanceAdjustment;
- }
-
- Map<ChargeDefinition, CostComponent> getCostComponents() {
- return costComponents;
- }
-
- public Stream<Map.Entry<ChargeDefinition, CostComponent>> stream() {
- return costComponents.entrySet().stream()
- .filter(costComponentEntry -> costComponentEntry.getValue().getAmount().compareTo(BigDecimal.ZERO) != 0);
- }
-
- BigDecimal getBalanceAdjustment() {
- return balanceAdjustment;
- }
-}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapper.java b/service/src/main/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapper.java
index 2bd9e57..851bedd 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapper.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapper.java
@@ -25,6 +25,7 @@
import javax.annotation.Nonnull;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
@@ -51,12 +52,16 @@
productAccountAssignments.stream().map(ProductMapper::mapAccountAssignmentEntity));
}
- public String mapOrThrow(final @Nonnull String accountDesignator) {
+ public Optional<String> map(final @Nonnull String accountDesignator) {
return allAccountAssignmentsAsStream()
- .filter(x -> x.getDesignator().equals(accountDesignator))
- .findFirst()
- .map(AccountAssignment::getAccountIdentifier)
- .orElseThrow(() -> ServiceException.badRequest("A required account designator was not set ''{0}''.", accountDesignator));
+ .filter(x -> x.getDesignator().equals(accountDesignator))
+ .findFirst()
+ .map(AccountAssignment::getAccountIdentifier);
+ }
+
+ public String mapOrThrow(final @Nonnull String accountDesignator) {
+ return map(accountDesignator).orElseThrow(() ->
+ ServiceException.badRequest("A required account designator was not set ''{0}''.", accountDesignator));
}
public Stream<AccountAssignment> getLedgersNeedingAccounts() {
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 62ddc51..25cd63d 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
@@ -57,6 +57,7 @@
final BigDecimal loanPaymentSize = CostComponentService.getLoanPaymentSize(
dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum(),
+ dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum(),
dataContextOfAction.getInterest(),
minorCurrencyUnitDigits,
scheduledCharges);
@@ -123,41 +124,43 @@
.sorted()
.collect(Collector.of(ArrayList::new, List::add, (left, right) -> { left.addAll(right); return left; }));
- BigDecimal balance = initialBalance.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
+ final SimulatedRunningBalances balances = new SimulatedRunningBalances();
final List<PlannedPayment> plannedPayments = new ArrayList<>();
for (int i = 0; i < sortedRepaymentPeriods.size(); i++)
{
final Period repaymentPeriod = sortedRepaymentPeriods.get(i);
- final BigDecimal currentLoanPaymentSize;
- if (repaymentPeriod.isDefined()) {
- // last repayment period: Force the proposed payment to "overhang". Cost component calculation
- // corrects last loan payment downwards but not upwards.
- if (i == sortedRepaymentPeriods.size() - 1)
- currentLoanPaymentSize = loanPaymentSize.add(BigDecimal.valueOf(sortedRepaymentPeriods.size()));
- else
- currentLoanPaymentSize = loanPaymentSize;
+ final BigDecimal requestedRepayment;
+ final BigDecimal requestedDisbursal;
+ if (i == 0)
+ { //First "period" is actually just the OPEN/APPROVE/DISBURSAL action set.
+ requestedRepayment = BigDecimal.ZERO;
+ requestedDisbursal = initialBalance.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
}
- else
- currentLoanPaymentSize = BigDecimal.ZERO;
+ else if (i == sortedRepaymentPeriods.size() - 1)
+ { //Last repayment period: Fill the proposed payment out to the remaining balance of the loan.
+ requestedRepayment = loanPaymentSize; //TODO: wrong doesn't include last period of interest.
+ requestedDisbursal = BigDecimal.ZERO;
+ }
+ else {
+ requestedRepayment = loanPaymentSize;
+ requestedDisbursal = BigDecimal.ZERO;
+ }
final SortedSet<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ final PaymentBuilder paymentBuilder =
CostComponentService.getCostComponentsForScheduledCharges(
Collections.emptyMap(),
scheduledChargesInPeriod,
initialBalance,
- balance,
- currentLoanPaymentSize,
+ balances,
+ loanPaymentSize,
+ requestedDisbursal,
+ requestedRepayment,
interest,
minorCurrencyUnitDigits,
false);
- final PlannedPayment plannedPayment = new PlannedPayment();
- plannedPayment.setCostComponents(new ArrayList<>(costComponentsForRepaymentPeriod.getCostComponents().values()));
- plannedPayment.setDate(repaymentPeriod.getEndDateAsString());
- balance = balance.add(costComponentsForRepaymentPeriod.getBalanceAdjustment());
- plannedPayment.setRemainingPrincipal(balance);
- plannedPayments.add(plannedPayment);
+ plannedPayments.add(paymentBuilder.accumulatePlannedPayment(balances));
}
return plannedPayments;
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/PaymentBuilder.java b/service/src/main/java/io/mifos/individuallending/internal/service/PaymentBuilder.java
new file mode 100644
index 0000000..f158dee
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/PaymentBuilder.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * 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 com.google.common.collect.Sets;
+import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
+import io.mifos.portfolio.api.v1.domain.Payment;
+import io.mifos.portfolio.service.internal.util.ChargeInstance;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+public class PaymentBuilder {
+ private final RunningBalances prePaymentBalances;
+ private final Map<ChargeDefinition, CostComponent> costComponents;
+ private final Map<String, BigDecimal> balanceAdjustments;
+ private final boolean accrualAccounting;
+
+ PaymentBuilder(final RunningBalances prePaymentBalances,
+ final boolean accrualAccounting) {
+ this.prePaymentBalances = prePaymentBalances;
+ this.costComponents = new HashMap<>();
+ this.balanceAdjustments = new HashMap<>();
+ this.accrualAccounting = accrualAccounting;
+ }
+
+ public Payment buildPayment(final Action action, final Set<String> forAccountDesignators) {
+
+ if (!forAccountDesignators.isEmpty()) {
+ final Stream<Map.Entry<ChargeDefinition, CostComponent>> costComponentStream = stream()
+ .filter(costComponentEntry -> chargeReferencesAccountDesignators(
+ costComponentEntry.getKey(),
+ action,
+ forAccountDesignators));
+
+ final List<CostComponent> costComponentList = costComponentStream
+ .map(costComponentEntry -> new CostComponent(
+ costComponentEntry.getKey().getIdentifier(),
+ costComponentEntry.getValue().getAmount()))
+ .collect(Collectors.toList());
+
+ return new Payment(costComponentList, balanceAdjustments);
+ }
+ else {
+ return buildPayment();
+ }
+
+ }
+
+ private Payment buildPayment() {
+ final Stream<Map.Entry<ChargeDefinition, CostComponent>> costComponentStream = stream();
+
+ final List<CostComponent> costComponentList = costComponentStream
+ .map(costComponentEntry -> new CostComponent(
+ costComponentEntry.getKey().getIdentifier(),
+ costComponentEntry.getValue().getAmount()))
+ .collect(Collectors.toList());
+
+ return new Payment(costComponentList, balanceAdjustments);
+ }
+
+ PlannedPayment accumulatePlannedPayment(final SimulatedRunningBalances balances) {
+ final Payment payment = buildPayment();
+ balanceAdjustments.forEach(balances::adjustBalance);
+ final Map<String, BigDecimal> balancesCopy = balances.snapshot();
+
+ return new PlannedPayment(payment, balancesCopy);
+ }
+
+ public List<ChargeInstance> buildCharges(
+ final Action action,
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
+ return stream()
+ .map(entry -> mapCostComponentEntryToChargeInstance(action, entry, designatorToAccountIdentifierMapper))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toList());
+ }
+
+ BigDecimal getBalanceAdjustment(final String accountDesignator) {
+ return balanceAdjustments.getOrDefault(accountDesignator, BigDecimal.ZERO);
+ }
+
+ void adjustBalances(
+ final Action action,
+ final ChargeDefinition chargeDefinition,
+ final BigDecimal chargeAmount) {
+ BigDecimal adjustedChargeAmount = BigDecimal.ZERO;
+ if (this.accrualAccounting) {
+ if (chargeIsAccrued(chargeDefinition)) {
+ if (Action.valueOf(chargeDefinition.getAccrueAction()) == action) {
+ final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getFromAccountDesignator(), chargeAmount);
+ adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getAccrualAccountDesignator(), maxDebit);
+
+ this.addToBalance(chargeDefinition.getFromAccountDesignator(), adjustedChargeAmount.negate());
+ this.addToBalance(chargeDefinition.getAccrualAccountDesignator(), adjustedChargeAmount);
+ } else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
+ final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getAccrualAccountDesignator(), chargeAmount);
+ adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getToAccountDesignator(), maxDebit);
+
+ this.addToBalance(chargeDefinition.getAccrualAccountDesignator(), adjustedChargeAmount.negate());
+ this.addToBalance(chargeDefinition.getToAccountDesignator(), adjustedChargeAmount);
+ }
+ } else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
+ final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getFromAccountDesignator(), chargeAmount);
+ adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getToAccountDesignator(), maxDebit);
+
+ this.addToBalance(chargeDefinition.getFromAccountDesignator(), adjustedChargeAmount.negate());
+ this.addToBalance(chargeDefinition.getToAccountDesignator(), adjustedChargeAmount);
+ }
+ }
+ else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
+ final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getFromAccountDesignator(), chargeAmount);
+ adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getToAccountDesignator(), maxDebit);
+
+ this.addToBalance(chargeDefinition.getFromAccountDesignator(), adjustedChargeAmount.negate());
+ this.addToBalance(chargeDefinition.getToAccountDesignator(), adjustedChargeAmount);
+ }
+
+
+ addToCostComponent(chargeDefinition, adjustedChargeAmount);
+ }
+
+ private static boolean chargeIsAccrued(final ChargeDefinition chargeDefinition) {
+ return chargeDefinition.getAccrualAccountDesignator() != null;
+ }
+
+ void addToBalance(
+ final String accountDesignator,
+ final BigDecimal chargeAmount) {
+ final BigDecimal currentAdjustment = balanceAdjustments.getOrDefault(accountDesignator, BigDecimal.ZERO);
+ final BigDecimal newAdjustment = currentAdjustment.add(chargeAmount);
+ balanceAdjustments.put(accountDesignator, newAdjustment);
+ }
+
+ void addToCostComponent(
+ final ChargeDefinition chargeDefinition,
+ final BigDecimal amount) {
+ final CostComponent costComponent = costComponents
+ .computeIfAbsent(chargeDefinition, PaymentBuilder::constructEmptyCostComponent);
+ costComponent.setAmount(costComponent.getAmount().add(amount));
+ }
+
+ private Stream<Map.Entry<ChargeDefinition, CostComponent>> stream() {
+ return costComponents.entrySet().stream()
+ .filter(costComponentEntry -> costComponentEntry.getValue().getAmount().compareTo(BigDecimal.ZERO) != 0);
+ }
+
+
+ private static boolean chargeReferencesAccountDesignators(
+ final ChargeDefinition chargeDefinition,
+ final Action action,
+ final Set<String> forAccountDesignators) {
+ final Set<String> accountsToCompare = Sets.newHashSet(
+ chargeDefinition.getFromAccountDesignator(),
+ chargeDefinition.getToAccountDesignator()
+ );
+ if (chargeDefinition.getAccrualAccountDesignator() != null)
+ accountsToCompare.add(chargeDefinition.getAccrualAccountDesignator());
+
+ return !Sets.intersection(accountsToCompare, forAccountDesignators).isEmpty();
+ }
+
+
+ private static CostComponent constructEmptyCostComponent(final ChargeDefinition chargeDefinition) {
+ final CostComponent ret = new CostComponent();
+ ret.setChargeIdentifier(chargeDefinition.getIdentifier());
+ ret.setAmount(BigDecimal.ZERO);
+ return ret;
+ }
+
+ private static Optional<ChargeInstance> mapCostComponentEntryToChargeInstance(
+ final Action action,
+ final Map.Entry<ChargeDefinition, CostComponent> costComponentEntry,
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
+ final ChargeDefinition chargeDefinition = costComponentEntry.getKey();
+ final BigDecimal chargeAmount = costComponentEntry.getValue().getAmount();
+
+ if (chargeIsAccrued(chargeDefinition)) {
+ if (Action.valueOf(chargeDefinition.getAccrueAction()) == action)
+ return Optional.of(new ChargeInstance(
+ designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
+ designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
+ chargeAmount));
+ else if (Action.valueOf(chargeDefinition.getChargeAction()) == action)
+ return Optional.of(new ChargeInstance(
+ designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
+ designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
+ chargeAmount));
+ else
+ return Optional.empty();
+ }
+ else if (Action.valueOf(chargeDefinition.getChargeAction()) == action)
+ return Optional.of(new ChargeInstance(
+ designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
+ designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
+ chargeAmount));
+ else
+ return Optional.empty();
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/RealRunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/RealRunningBalances.java
new file mode 100644
index 0000000..761c963
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/RealRunningBalances.java
@@ -0,0 +1,58 @@
+/*
+ * 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.AccountDesignators;
+import io.mifos.portfolio.service.internal.util.AccountingAdapter;
+import net.jodah.expiringmap.ExpirationPolicy;
+import net.jodah.expiringmap.ExpiringMap;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Myrle Krantz
+ */
+public class RealRunningBalances implements RunningBalances {
+ private final ExpiringMap<String, BigDecimal> realBalanceCache;
+
+
+ RealRunningBalances(
+ final AccountingAdapter accountingAdapter,
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
+ this.realBalanceCache = ExpiringMap.builder()
+ .maxSize(20)
+ .expirationPolicy(ExpirationPolicy.CREATED)
+ .expiration(30,TimeUnit.SECONDS)
+ .entryLoader((String accountDesignator) -> {
+ final Optional<String> accountIdentifier;
+ if (accountDesignator.equals(AccountDesignators.ENTRY)) {
+ accountIdentifier = designatorToAccountIdentifierMapper.map(accountDesignator);
+ }
+ else {
+ accountIdentifier = Optional.of(designatorToAccountIdentifierMapper.mapOrThrow(accountDesignator));
+ }
+ return accountIdentifier.map(accountingAdapter::getCurrentBalance).orElse(BigDecimal.ZERO);
+ })
+ .build();
+ }
+
+ @Override
+ public BigDecimal getBalance(final String accountDesignator) {
+ return realBalanceCache.get(accountDesignator);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/RunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/RunningBalances.java
new file mode 100644
index 0000000..08197f6
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/RunningBalances.java
@@ -0,0 +1,66 @@
+/*
+ * 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.AccountDesignators;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Myrle Krantz
+ */
+public interface RunningBalances {
+ Map<String, BigDecimal> ACCOUNT_SIGNS = new HashMap<String, BigDecimal>() {{
+ final BigDecimal negative = BigDecimal.valueOf(-1);
+ final BigDecimal positive = BigDecimal.valueOf(1);
+
+ this.put(AccountDesignators.CUSTOMER_LOAN, negative);
+ this.put(AccountDesignators.LOAN_FUNDS_SOURCE, negative);
+ this.put(AccountDesignators.PROCESSING_FEE_INCOME, positive);
+ this.put(AccountDesignators.ORIGINATION_FEE_INCOME, positive);
+ this.put(AccountDesignators.DISBURSEMENT_FEE_INCOME, positive);
+ this.put(AccountDesignators.INTEREST_INCOME, positive);
+ this.put(AccountDesignators.INTEREST_ACCRUAL, positive);
+ this.put(AccountDesignators.LATE_FEE_INCOME, positive);
+ this.put(AccountDesignators.LATE_FEE_ACCRUAL, positive);
+ this.put(AccountDesignators.ARREARS_ALLOWANCE, positive);
+ this.put(AccountDesignators.ENTRY, positive);
+ }};
+
+ BigDecimal getBalance(final String accountDesignator);
+
+ default BigDecimal getMaxDebit(final String accountDesignator, final BigDecimal amount) {
+ if (accountDesignator.equals(AccountDesignators.ENTRY))
+ return amount;
+
+ if (ACCOUNT_SIGNS.get(accountDesignator).signum() == -1)
+ return amount;
+ else
+ return amount.min(getBalance(accountDesignator));
+ }
+
+ default BigDecimal getMaxCredit(final String accountDesignator, final BigDecimal amount) {
+ if (accountDesignator.equals(AccountDesignators.ENTRY))
+ return amount; //don't guard the entry account.
+
+ if (ACCOUNT_SIGNS.get(accountDesignator).signum() != -1)
+ return amount;
+ else
+ return amount.min(getBalance(accountDesignator));
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/SimulatedRunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/SimulatedRunningBalances.java
new file mode 100644
index 0000000..3c4ce83
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/SimulatedRunningBalances.java
@@ -0,0 +1,47 @@
+/*
+ * 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.HashMap;
+import java.util.Map;
+
+/**
+ * @author Myrle Krantz
+ */
+public class SimulatedRunningBalances implements RunningBalances {
+ final private Map<String, BigDecimal> balances;
+
+ SimulatedRunningBalances() {
+ this.balances = new HashMap<>();
+ }
+
+ public BigDecimal getBalance(final String accountDesignator) {
+ return balances.getOrDefault(accountDesignator, BigDecimal.ZERO);
+ }
+
+ 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));
+ balances.put(key, newValue);
+ }
+
+ Map<String, BigDecimal> snapshot() {
+ return new HashMap<>(balances);
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
index d09fab1..cff6143 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
@@ -90,10 +90,6 @@
private static Boolean readOnlyLegacyMapper(final String identifier) {
switch (identifier) {
- case LOAN_FUNDS_ALLOCATION_ID:
- return true;
- case RETURN_DISBURSEMENT_ID:
- return true;
case INTEREST_ID:
return false;
case ALLOW_FOR_WRITE_OFF_ID:
@@ -104,16 +100,12 @@
return false;
case DISBURSE_PAYMENT_ID:
return false;
- case TRACK_DISBURSAL_PAYMENT_ID:
- return false;
case LOAN_ORIGINATION_FEE_ID:
return true;
case PROCESSING_FEE_ID:
return true;
case REPAYMENT_ID:
return false;
- case TRACK_RETURN_PRINCIPAL_ID:
- return false;
default:
return false;
}
@@ -126,14 +118,12 @@
return from.getProportionalTo();
switch (identifier) {
- case LOAN_FUNDS_ALLOCATION_ID:
- return ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue();
case LOAN_ORIGINATION_FEE_ID:
return ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue();
case PROCESSING_FEE_ID:
return ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue();
case LATE_FEE_ID:
- return ChargeProportionalDesignator.REPAYMENT_DESIGNATOR.getValue();
+ return ChargeProportionalDesignator.CONTRACTUAL_REPAYMENT_DESIGNATOR.getValue();
default:
return ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue();
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
index c004eb4..414d38e 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
@@ -18,7 +18,7 @@
import io.mifos.core.lang.ServiceException;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.CasePage;
-import io.mifos.portfolio.api.v1.domain.CostComponent;
+import io.mifos.portfolio.api.v1.domain.Payment;
import io.mifos.portfolio.service.internal.mapper.CaseMapper;
import io.mifos.portfolio.service.internal.pattern.PatternFactoryRegistry;
import io.mifos.portfolio.service.internal.repository.CaseEntity;
@@ -133,12 +133,12 @@
return this.findByIdentifier(productIdentifier, caseIdentifier).isPresent();
}
- public List<CostComponent> getActionCostComponentsForCase(final String productIdentifier,
- final String caseIdentifier,
- final String actionIdentifier,
- final LocalDateTime localDateTime,
- final Set<String> forAccountDesignatorsList,
- final BigDecimal forPaymentSize) {
+ public Payment getActionCostComponentsForCase(final String productIdentifier,
+ final String caseIdentifier,
+ final String actionIdentifier,
+ final LocalDateTime localDateTime,
+ final Set<String> forAccountDesignatorsList,
+ final BigDecimal forPaymentSize) {
return getPatternFactoryOrThrow(productIdentifier).getCostComponentsForAction(
productIdentifier,
caseIdentifier,
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
index c95b51e..df851db 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
@@ -156,10 +156,12 @@
public BigDecimal getCurrentBalance(final String accountIdentifier) {
try {
final Account account = ledgerManager.findAccount(accountIdentifier);
+ if (account == null)
+ throw ServiceException.internalError("Could not find the account with identifier ''{0}''", accountIdentifier);
return BigDecimal.valueOf(account.getBalance());
}
catch (final AccountNotFoundException e) {
- throw ServiceException.internalError("Could not found the account with the identifier ''{0}''", accountIdentifier);
+ throw ServiceException.internalError("Could not find the account with identifier ''{0}''", accountIdentifier);
}
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
index a2471c2..c24e7de 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
@@ -26,7 +26,7 @@
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.CasePage;
import io.mifos.portfolio.api.v1.domain.Command;
-import io.mifos.portfolio.api.v1.domain.CostComponent;
+import io.mifos.portfolio.api.v1.domain.Payment;
import io.mifos.portfolio.service.internal.checker.CaseChecker;
import io.mifos.portfolio.service.internal.command.ChangeCaseCommand;
import io.mifos.portfolio.service.internal.command.CreateCaseCommand;
@@ -45,7 +45,6 @@
import java.math.BigDecimal;
import java.time.Clock;
import java.time.LocalDateTime;
-import java.util.List;
import java.util.Set;
/**
@@ -194,12 +193,13 @@
produces = MediaType.APPLICATION_JSON_VALUE
)
@ResponseBody
- List<CostComponent> getCostComponentsForAction(@PathVariable("productidentifier") final String productIdentifier,
- @PathVariable("caseidentifier") final String caseIdentifier,
- @PathVariable("actionidentifier") final String actionIdentifier,
- @RequestParam(value="fordatetime", required = false, defaultValue = "") final @ValidLocalDateTimeString String forDateTimeString,
- @RequestParam(value="touchingaccounts", required = false, defaultValue = "") final Set<String> forAccountDesignators,
- @RequestParam(value="forpaymentsize", required = false, defaultValue = "") final BigDecimal forPaymentSize)
+ Payment getCostComponentsForAction(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("caseidentifier") final String caseIdentifier,
+ @PathVariable("actionidentifier") final String actionIdentifier,
+ @RequestParam(value="fordatetime", required = false, defaultValue = "") final @ValidLocalDateTimeString String forDateTimeString,
+ @RequestParam(value="touchingaccounts", required = false, defaultValue = "") final Set<String> forAccountDesignators,
+ @RequestParam(value="forpaymentsize", required = false, defaultValue = "") final BigDecimal forPaymentSize)
{
checkThatCaseExists(productIdentifier, caseIdentifier);
diff --git a/service/src/main/java/io/mifos/products/spi/PatternFactory.java b/service/src/main/java/io/mifos/products/spi/PatternFactory.java
index dd0e555..9c153be 100644
--- a/service/src/main/java/io/mifos/products/spi/PatternFactory.java
+++ b/service/src/main/java/io/mifos/products/spi/PatternFactory.java
@@ -16,10 +16,7 @@
package io.mifos.products.spi;
-import io.mifos.portfolio.api.v1.domain.Case;
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
-import io.mifos.portfolio.api.v1.domain.CostComponent;
-import io.mifos.portfolio.api.v1.domain.Pattern;
+import io.mifos.portfolio.api.v1.domain.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -38,7 +35,7 @@
void changeParameters(Long caseId, String parameters);
Optional<String> getParameters(Long caseId, int minorCurrencyUnitDigits);
Set<String> getNextActionsForState(Case.State state);
- List<CostComponent> getCostComponentsForAction(
+ Payment getCostComponentsForAction(
String productIdentifier,
String caseIdentifier,
String actionIdentifier,
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
index f2aa460..4a639c9 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
@@ -15,6 +15,7 @@
*/
package io.mifos.individuallending.internal.service;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import org.junit.Assert;
import org.junit.Test;
@@ -22,11 +23,9 @@
import org.junit.runners.Parameterized;
import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
+import java.util.*;
-import static io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR;
+import static io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR;
import static io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR;
/**
@@ -40,7 +39,7 @@
BigDecimal maximumBalance = BigDecimal.ZERO;
BigDecimal runningBalance = BigDecimal.ZERO;
BigDecimal loanPaymentSize = BigDecimal.ZERO;
- BigDecimal expectedAmount = BigDecimal.ZERO;
+ BigDecimal expectedAmount = BigDecimal.ONE;
private TestCase(String description) {
this.description = description;
@@ -70,6 +69,18 @@
this.expectedAmount = expectedAmount;
return this;
}
+
+ @Override
+ public String toString() {
+ return "TestCase{" +
+ "description='" + description + '\'' +
+ ", chargeProportionalDesignator=" + chargeProportionalDesignator +
+ ", maximumBalance=" + maximumBalance +
+ ", runningBalance=" + runningBalance +
+ ", loanPaymentSize=" + loanPaymentSize +
+ ", expectedAmount=" + expectedAmount +
+ '}';
+ }
}
@Parameterized.Parameters
@@ -77,9 +88,9 @@
final Collection<CostComponentServiceTest.TestCase> ret = new ArrayList<>();
ret.add(new TestCase("simple"));
ret.add(new TestCase("distribution fee")
- .chargeProportionalDesignator(PRINCIPAL_ADJUSTMENT_DESIGNATOR)
+ .chargeProportionalDesignator(REQUESTED_DISBURSEMENT_DESIGNATOR)
.maximumBalance(BigDecimal.valueOf(2000))
- .loanPaymentSize(BigDecimal.valueOf(-2000))
+ .loanPaymentSize(BigDecimal.valueOf(2000))
.expectedAmount(BigDecimal.valueOf(2000)));
ret.add(new TestCase("origination fee")
.chargeProportionalDesignator(RUNNING_BALANCE_DESIGNATOR)
@@ -96,14 +107,18 @@
@Test
public void getAmountProportionalTo() {
+ final SimulatedRunningBalances runningBalances = new SimulatedRunningBalances();
+ runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN, testCase.runningBalance.negate());
final BigDecimal amount = CostComponentService.getAmountProportionalTo(
testCase.chargeProportionalDesignator,
testCase.maximumBalance,
- testCase.runningBalance,
+ runningBalances,
testCase.loanPaymentSize,
- Collections.emptyMap());
+ testCase.loanPaymentSize,
+ testCase.loanPaymentSize,
+ new PaymentBuilder(runningBalances, false));
- Assert.assertEquals(testCase.expectedAmount, amount);
+ Assert.assertEquals(testCase.toString(), testCase.expectedAmount, amount);
}
}
\ No newline at end of file
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 4daaedc..a31f3e0 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,7 +24,10 @@
import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
-import io.mifos.portfolio.api.v1.domain.*;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
+import io.mifos.portfolio.api.v1.domain.PaymentCycle;
+import io.mifos.portfolio.api.v1.domain.TermRange;
import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
import io.mifos.portfolio.service.internal.repository.CaseEntity;
import io.mifos.portfolio.service.internal.repository.ProductEntity;
@@ -94,14 +97,10 @@
private BigDecimal interest;
private Set<String> expectedChargeIdentifiers = new HashSet<>(Arrays.asList(
PROCESSING_FEE_ID,
- LOAN_FUNDS_ALLOCATION_ID,
- RETURN_DISBURSEMENT_ID,
LOAN_ORIGINATION_FEE_ID,
INTEREST_ID,
DISBURSEMENT_FEE_ID,
REPAYMENT_ID,
- TRACK_DISBURSAL_PAYMENT_ID,
- TRACK_RETURN_PRINCIPAL_ID,
DISBURSE_PAYMENT_ID,
LATE_FEE_ID
));
@@ -189,8 +188,8 @@
caseParameters.setTermRange(new TermRange(ChronoUnit.WEEKS, 3));
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, 0, null, null));
- final ChargeDefinition processingFeeCharge = getFixedSingleChargeDefinition(10.0, Action.OPEN, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME);
- final ChargeDefinition loanOriginationFeeCharge = getFixedSingleChargeDefinition(100.0, Action.APPROVE, LOAN_ORIGINATION_FEE_ID, AccountDesignators.ORIGINATION_FEE_INCOME);
+ final ChargeDefinition processingFeeCharge = getFixedSingleChargeDefinition(10.0, Action.DISBURSE, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME);
+ final ChargeDefinition loanOriginationFeeCharge = getFixedSingleChargeDefinition(100.0, Action.DISBURSE, LOAN_ORIGINATION_FEE_ID, AccountDesignators.ORIGINATION_FEE_INCOME);
final List<ChargeDefinition> defaultChargesWithFeesReplaced =
charges().stream().map(x -> {
switch (x.getIdentifier()) {
@@ -209,9 +208,7 @@
.initialDisbursementDate(initialDisbursementDate)
.chargeDefinitions(defaultChargesWithFeesReplaced)
.interest(BigDecimal.valueOf(1))
- .expectChargeInstancesForActionDatePair(Action.OPEN, initialDisbursementDate, Collections.singletonList(processingFeeCharge))
- .expectChargeInstancesForActionDatePair(Action.APPROVE, initialDisbursementDate,
- Collections.singletonList(loanOriginationFeeCharge));
+ .expectChargeInstancesForActionDatePair(Action.DISBURSE, initialDisbursementDate, Arrays.asList(processingFeeCharge, loanOriginationFeeCharge));
}
private static TestCase yearLoanTestCase()
@@ -267,7 +264,7 @@
ret.setChargeAction(action.name());
ret.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
ret.setProportionalTo(null);
- ret.setFromAccountDesignator(AccountDesignators.ENTRY);
+ ret.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN);
ret.setToAccountDesignator(feeAccountDesignator);
ret.setForCycleSizeUnit(null);
return ret;
@@ -318,38 +315,42 @@
final Set<BigDecimal> customerRepayments = Stream.iterate(1, x -> x + 1).limit(allPlannedPayments.size() - 1)
.map(x ->
{
- final BigDecimal costComponentSum = allPlannedPayments.get(x).getCostComponents().stream()
- .filter(this::includeCostComponentsInSumCheck)
+ final BigDecimal valueOfRepaymentCostComponent = allPlannedPayments.get(x).getPayment().getCostComponents().stream()
+ .filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID))
.map(CostComponent::getAmount)
.reduce(BigDecimal::add)
.orElse(BigDecimal.ZERO);
- final BigDecimal valueOfPrincipleTrackingCostComponent = allPlannedPayments.get(x).getCostComponents().stream()
- .filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID))
+ final BigDecimal valueOfInterestCostComponent = allPlannedPayments.get(x).getPayment().getCostComponents().stream()
+ .filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.INTEREST_ID))
.map(CostComponent::getAmount)
.reduce(BigDecimal::add)
.orElse(BigDecimal.ZERO);
- final BigDecimal principalDifference = allPlannedPayments.get(x-1).getRemainingPrincipal().subtract(allPlannedPayments.get(x).getRemainingPrincipal());
- Assert.assertEquals(valueOfPrincipleTrackingCostComponent, principalDifference);
+ final BigDecimal principalDifference = allPlannedPayments.get(x-1).getBalances().get(AccountDesignators.CUSTOMER_LOAN).subtract(allPlannedPayments.get(x).getBalances().get(AccountDesignators.CUSTOMER_LOAN));
+ Assert.assertEquals("Checking payment " + x, valueOfRepaymentCostComponent.subtract(valueOfInterestCostComponent), principalDifference);
Assert.assertNotEquals("Remaining principle should always be positive or zero.",
- allPlannedPayments.get(x).getRemainingPrincipal().signum(), -1);
- final boolean containsLateFee = allPlannedPayments.get(x).getCostComponents().stream().anyMatch(y -> y.getChargeIdentifier().equals(LATE_FEE_ID));
+ allPlannedPayments.get(x).getBalances().get(AccountDesignators.CUSTOMER_LOAN).signum(), -1);
+ final boolean containsLateFee = allPlannedPayments.get(x).getPayment().getCostComponents().stream().anyMatch(y -> y.getChargeIdentifier().equals(LATE_FEE_ID));
Assert.assertFalse("Late fee should not be included in planned payments", containsLateFee);
- return costComponentSum;
+ return valueOfRepaymentCostComponent;
}
).collect(Collectors.toSet());
//All entries should have the correct scale.
allPlannedPayments.forEach(x -> {
- x.getCostComponents().forEach(y -> Assert.assertEquals(testCase.minorCurrencyUnitDigits, y.getAmount().scale()));
- Assert.assertEquals(testCase.minorCurrencyUnitDigits, x.getRemainingPrincipal().scale());
- final int uniqueChargeIdentifierCount = x.getCostComponents().stream()
+ x.getPayment().getCostComponents().forEach(y -> Assert.assertEquals(testCase.minorCurrencyUnitDigits, y.getAmount().scale()));
+ Assert.assertEquals(testCase.minorCurrencyUnitDigits, x.getBalances().get(AccountDesignators.CUSTOMER_LOAN).scale());
+ final int uniqueChargeIdentifierCount = x.getPayment().getCostComponents().stream()
.map(CostComponent::getChargeIdentifier)
.collect(Collectors.toSet())
.size();
Assert.assertEquals("There should be only one cost component per charge per planned payment.",
- x.getCostComponents().size(), uniqueChargeIdentifierCount);
+ x.getPayment().getCostComponents().size(), uniqueChargeIdentifierCount);
});
+ Assert.assertEquals("Final balance should be zero.",
+ BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
+ allPlannedPayments.get(allPlannedPayments.size()-1).getBalances().get(AccountDesignators.CUSTOMER_LOAN));
+
//All customer payments should be within one percent of each other.
final Optional<BigDecimal> maxPayment = customerRepayments.stream().max(BigDecimal::compareTo);
final Optional<BigDecimal> minPayment = customerRepayments.stream().min(BigDecimal::compareTo);
@@ -359,10 +360,6 @@
Assert.assertTrue("Percent difference = " + percentDifference + ", max = " + maxPayment.get() + ", min = " + minPayment.get(),
percentDifference < 0.01);
- //Final balance should be zero.
- Assert.assertEquals(BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
- allPlannedPayments.get(allPlannedPayments.size()-1).getRemainingPrincipal());
-
//All charge identifiers should be associated with a name on the returned page.
final Set<String> resultChargeIdentifiers = firstPage.getChargeNames().stream()
.map(ChargeName::getIdentifier)
@@ -371,22 +368,6 @@
Assert.assertEquals(testCase.expectedChargeIdentifiers, resultChargeIdentifiers);
}
- private boolean includeCostComponentsInSumCheck(CostComponent costComponent) {
- switch (costComponent.getChargeIdentifier()) {
- case ChargeIdentifiers.INTEREST_ID:
- case ChargeIdentifiers.DISBURSEMENT_FEE_ID:
- case ChargeIdentifiers.TRACK_DISBURSAL_PAYMENT_ID:
- case ChargeIdentifiers.LATE_FEE_ID:
- case ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID:
- case ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID:
- case ChargeIdentifiers.PROCESSING_FEE_ID:
- return true;
- default:
- return false;
-
- }
- }
-
@Test
public void getScheduledCharges() {
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
@@ -423,7 +404,8 @@
new ActionDatePair(scheduledCharge.getScheduledAction().action, scheduledCharge.getScheduledAction().when),
Collectors.mapping(ScheduledCharge::getChargeDefinition, Collectors.toSet())));
- testCase.chargeDefinitionsForActions.forEach((key, value) -> value.forEach(x -> Assert.assertTrue(searchableScheduledCharges.get(key).contains(x))));
+ testCase.chargeDefinitionsForActions.forEach((key, value) ->
+ value.forEach(x -> Assert.assertTrue(searchableScheduledCharges.get(key).contains(x))));
}
private double percentDifference(final BigDecimal maxPayment, final BigDecimal minPayment) {
diff --git a/shared.gradle b/shared.gradle
index 410d57d..19e1491 100644
--- a/shared.gradle
+++ b/shared.gradle
@@ -14,7 +14,8 @@
mifosrhythm : '0.1.0-BUILD-SNAPSHOT',
mifoscustomer : '0.1.0-BUILD-SNAPSHOT',
validator : '5.3.0.Final',
- javamoneylib : '0.9-SNAPSHOT'
+ javamoneylib : '0.9-SNAPSHOT',
+ expiringmap : '0.5.8'
]
apply plugin: 'java'