Merge pull request #10 from KuelapInc/plannedPaymentsAsCostoComponents
Planned payments as costo components
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/client/IndividualLending.java b/api/src/main/java/io/mifos/individuallending/api/v1/client/IndividualLending.java
index cfafd66..c401122 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/client/IndividualLending.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/client/IndividualLending.java
@@ -15,15 +15,16 @@
*/
package io.mifos.individuallending.api.v1.client;
-import io.mifos.portfolio.api.v1.domain.CasePage;
-import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
import io.mifos.core.api.util.CustomFeignClientsConfiguration;
+import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
+import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
+import io.mifos.individuallending.api.v1.domain.product.LossProvisionConfiguration;
+import io.mifos.portfolio.api.v1.domain.CasePage;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.stream.Stream;
/**
* @author Myrle Krantz
@@ -31,17 +32,58 @@
@SuppressWarnings("unused")
@FeignClient (value = "portfolio-v1", path = "/portfolio/v1", configuration = CustomFeignClientsConfiguration.class)
public interface IndividualLending {
+
+ @RequestMapping(
+ value = "/individuallending/products/{productidentifier}/lossprovisionconfiguration",
+ method = RequestMethod.PUT,
+ produces = MediaType.ALL_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE
+ )
+ void changeLossProvisionConfiguration(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @RequestBody LossProvisionConfiguration lossProvisionConfiguration);
+
+ @RequestMapping(
+ value = "/individuallending/products/{productidentifier}/lossprovisionconfiguration",
+ method = RequestMethod.GET,
+ produces = MediaType.ALL_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE
+ )
+ LossProvisionConfiguration getLossProvisionConfiguration(
+ @PathVariable("productidentifier") final String productIdentifier);
+
@RequestMapping(
value = "/individuallending/products/{productidentifier}/cases/{caseidentifier}/plannedpayments",
method = RequestMethod.GET,
produces = MediaType.ALL_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
- PlannedPaymentPage getPaymentScheduleForCase(@PathVariable("productidentifier") final String productIdentifier,
- @PathVariable("caseidentifier") final String caseIdentifier,
- @RequestParam(value = "pageIndex", required = false) final Integer pageIndex,
- @RequestParam(value = "size", required = false) final Integer size,
- @RequestParam(value = "initialDisbursalDate", required = false) final String initialDisbursalDate);
+ PlannedPaymentPage getPaymentScheduleForCase(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("caseidentifier") final String caseIdentifier,
+ @RequestParam(value = "pageIndex", required = false) final Integer pageIndex,
+ @RequestParam(value = "size", required = false) final Integer size,
+ @RequestParam(value = "initialDisbursalDate", required = false) final String initialDisbursalDate);
+
+ default Stream<PlannedPayment> getPaymentScheduleForCaseStream(
+ final String productIdentifier,
+ final String caseIdentifier,
+ final String initialDisbursalDate) {
+ final PlannedPaymentPage firstPage = this.getPaymentScheduleForCase(
+ productIdentifier,
+ caseIdentifier,
+ 0,
+ 10,
+ initialDisbursalDate);
+
+ final Integer pageCount = firstPage.getTotalPages();
+ // Sort column is always date and order always ascending so that the order and adjacency of account
+ // entries is always stable. This has the advantage that the set of account entries included in the
+ // stream is set the moment the first call to fetchAccountEntries (above) is made.
+ return Stream.iterate(0, (i) -> i + 1).limit(pageCount)
+ .map(i -> this.getPaymentScheduleForCase(productIdentifier, caseIdentifier, i, 10, initialDisbursalDate))
+ .flatMap(pageI -> pageI.getElements().stream());
+ }
@RequestMapping(
value = "/individuallending/customers/{customeridentifier}/cases",
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..fb78e89 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
@@ -20,17 +20,23 @@
*/
@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";
- String INTEREST_INCOME = "interest-income";
- String INTEREST_ACCRUAL = "interest-accrual";
- String LATE_FEE_INCOME = "late-fee-income";
- String LATE_FEE_ACCRUAL = "late-fee-accrual";
- String ARREARS_ALLOWANCE = "arrears-allowance";
- String ENTRY = "entry";
+ //These are maximum 3 characters because they are used to create account and ledger identifiers.
+ //Account and ledger identifiers are limited to 34 characters, and 32 characters respectively.
+ //These accounting identifiers are composed of the customer identifier, this identifier, and a counter.
+ String CUSTOMER_LOAN_GROUP = "cll";
+ String CUSTOMER_LOAN_PRINCIPAL = "clp";
+ String CUSTOMER_LOAN_INTEREST = "cli";
+ String CUSTOMER_LOAN_FEES = "clf";
+ String LOAN_FUNDS_SOURCE = "ls";
+ String PROCESSING_FEE_INCOME = "pfi";
+ String ORIGINATION_FEE_INCOME = "ofi";
+ String DISBURSEMENT_FEE_INCOME = "dfi";
+ String INTEREST_INCOME = "ii";
+ String INTEREST_ACCRUAL = "ia";
+ String LATE_FEE_INCOME = "lfi";
+ String LATE_FEE_ACCRUAL = "lfa";
+ String PRODUCT_LOSS_ALLOWANCE = "pa";
+ String GENERAL_LOSS_ALLOWANCE = "aa";
+ String GENERAL_EXPENSE = "ge";
+ String ENTRY = "ey";
}
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..8167c85 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,18 @@
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";
+ String REPAY_PRINCIPAL_NAME = "Repay principal";
+ String REPAY_PRINCIPAL_ID = "repay-principal";
+ String REPAY_INTEREST_NAME = "Repay interest";
+ String REPAY_INTEREST_ID = "repay-interest";
+ String REPAY_FEES_NAME = "Repay fees";
+ String REPAY_FEES_ID = "repay-fees";
+ String PROVISION_FOR_LOSSES_NAME = "Provision for losses";
+ String PROVISION_FOR_LOSSES_ID = "loss-provisioning";
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..e5d0716 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,12 @@
NOT_PROPORTIONAL("{notproportional}", 0),
MAXIMUM_BALANCE_DESIGNATOR("{maximumbalance}", 1),
RUNNING_BALANCE_DESIGNATOR("{runningbalance}", 2),
- PRINCIPAL_ADJUSTMENT_DESIGNATOR("{principaladjustment}", 3),
- REPAYMENT_DESIGNATOR("{repayment}", 4),
+ PRINCIPAL_DESIGNATOR("{principal}", 3),
+ REQUESTED_DISBURSEMENT_DESIGNATOR("{requesteddisbursement}", 4),
+ TO_ACCOUNT_DESIGNATOR("{toAccount}", 5),
+ FROM_ACCOUNT_DESIGNATOR("{fromAccount}", 6),
+ REQUESTED_REPAYMENT_DESIGNATOR("{requestedrepayment}", 7),
+ CONTRACTUAL_REPAYMENT_DESIGNATOR("{contractualrepayment}", 8),
;
private final String value;
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionConfiguration.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionConfiguration.java
new file mode 100644
index 0000000..aa3707f
--- /dev/null
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionConfiguration.java
@@ -0,0 +1,62 @@
+/*
+ * 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.product;
+
+import io.mifos.portfolio.api.v1.validation.ValidLossProvisionList;
+
+import java.util.List;
+import java.util.Objects;
+
+@SuppressWarnings("WeakerAccess")
+public class LossProvisionConfiguration {
+ @ValidLossProvisionList
+ private List<LossProvisionStep> lossProvisionSteps;
+
+ public LossProvisionConfiguration() {
+ }
+
+ public LossProvisionConfiguration(List<LossProvisionStep> lossProvisionSteps) {
+ this.lossProvisionSteps = lossProvisionSteps;
+ }
+
+ public List<LossProvisionStep> getLossProvisionSteps() {
+ return lossProvisionSteps;
+ }
+
+ public void setLossProvisionSteps(List<LossProvisionStep> lossProvisionSteps) {
+ this.lossProvisionSteps = lossProvisionSteps;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LossProvisionConfiguration that = (LossProvisionConfiguration) o;
+ return Objects.equals(lossProvisionSteps, that.lossProvisionSteps);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(lossProvisionSteps);
+ }
+
+ @Override
+ public String toString() {
+ return "LossProvisionConfiguration{" +
+ "lossProvisionSteps=" + lossProvisionSteps +
+ '}';
+ }
+}
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionStep.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionStep.java
new file mode 100644
index 0000000..664f669
--- /dev/null
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionStep.java
@@ -0,0 +1,83 @@
+/*
+ * 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.product;
+
+import org.hibernate.validator.constraints.Range;
+
+import javax.validation.constraints.DecimalMax;
+import javax.validation.constraints.DecimalMin;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+public class LossProvisionStep {
+ @Range(min = 0)
+ private int daysLate;
+
+ @DecimalMin(value = "0.00")
+ @DecimalMax(value = "100.00")
+ @NotNull
+ private BigDecimal percentProvision;
+
+ public LossProvisionStep() {
+ }
+
+ public LossProvisionStep(int daysLate, BigDecimal percentProvision) {
+ this.daysLate = daysLate;
+ this.percentProvision = percentProvision;
+ }
+
+ public int getDaysLate() {
+ return daysLate;
+ }
+
+ public void setDaysLate(int daysLate) {
+ this.daysLate = daysLate;
+ }
+
+ public BigDecimal getPercentProvision() {
+ return percentProvision;
+ }
+
+ public void setPercentProvision(BigDecimal percentProvision) {
+ this.percentProvision = percentProvision;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LossProvisionStep that = (LossProvisionStep) o;
+ return daysLate == that.daysLate &&
+ Objects.equals(percentProvision, that.percentProvision);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(daysLate, percentProvision);
+ }
+
+ @Override
+ public String toString() {
+ return "LossProvisionStep{" +
+ "daysLate=" + daysLate +
+ ", percentProvision=" + percentProvision +
+ '}';
+ }
+}
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanEventConstants.java b/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanEventConstants.java
index 89e1e4f..6264e74 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanEventConstants.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanEventConstants.java
@@ -22,6 +22,7 @@
String DESTINATION = "portfolio-v1";
String SELECTOR_NAME = "action";
+ String PUT_LOSS_PROVISION_STEPS = "put-loss-provision-steps";
String OPEN_INDIVIDUALLOAN_CASE = "open-individualloan-case";
String DENY_INDIVIDUALLOAN_CASE = "deny-individualloan-case";
String APPROVE_INDIVIDUALLOAN_CASE = "approve-individualloan-case";
@@ -34,6 +35,7 @@
String CLOSE_INDIVIDUALLOAN_CASE = "close-individualloan-case";
String RECOVER_INDIVIDUALLOAN_CASE = "recover-individualloan-case";
+ String SELECTOR_PUT_LOSS_PROVISION_STEPS = SELECTOR_NAME + " = '" + PUT_LOSS_PROVISION_STEPS + "'";
String SELECTOR_OPEN_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + OPEN_INDIVIDUALLOAN_CASE + "'";
String SELECTOR_DENY_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + DENY_INDIVIDUALLOAN_CASE + "'";
String SELECTOR_APPROVE_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + APPROVE_INDIVIDUALLOAN_CASE + "'";
@@ -45,4 +47,4 @@
String SELECTOR_WRITE_OFF_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + WRITE_OFF_INDIVIDUALLOAN_CASE + "'";
String SELECTOR_CLOSE_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + CLOSE_INDIVIDUALLOAN_CASE + "'";
String SELECTOR_RECOVER_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + RECOVER_INDIVIDUALLOAN_CASE + "'";
-}
+}
\ No newline at end of file
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/PermittableGroupIds.java b/api/src/main/java/io/mifos/portfolio/api/v1/PermittableGroupIds.java
index 85dab16..48b294c 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/PermittableGroupIds.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/PermittableGroupIds.java
@@ -21,6 +21,7 @@
@SuppressWarnings("unused")
public interface PermittableGroupIds {
String PRODUCT_OPERATIONS_MANAGEMENT = "portfolio__v1__products__enable";
+ String PRODUCT_LOSS_PROVISIONING_MANAGEMENT = "portfolio__v1__products__lossprov";
String PRODUCT_MANAGEMENT = "portfolio__v1__products";
String CASE_MANAGEMENT = "portfolio__v1__case";
}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/ChargeDefinitionIsReadOnly.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/ChargeDefinitionIsReadOnly.java
deleted file mode 100644
index e374850..0000000
--- a/api/src/main/java/io/mifos/portfolio/api/v1/client/ChargeDefinitionIsReadOnly.java
+++ /dev/null
@@ -1,22 +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.portfolio.api.v1.client;
-
-/**
- * @author Myrle Krantz
- */
-public class ChargeDefinitionIsReadOnly extends RuntimeException {
-}
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..9a63006 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;
@@ -48,15 +49,6 @@
List<Pattern> getAllPatterns();
@RequestMapping(
- value = "/patterns/{patternpackage}/charges/",
- method = RequestMethod.GET,
- produces = MediaType.ALL_VALUE,
- consumes = MediaType.APPLICATION_JSON_VALUE
- )
- List<ChargeDefinition> getAllDefaultChargeDefinitionsForPattern(
- @PathVariable("patternpackage") final String patternPackage);
-
- @RequestMapping(
value = "/products/",
method = RequestMethod.GET,
produces = MediaType.ALL_VALUE,
@@ -268,7 +260,6 @@
produces = MediaType.ALL_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
- @ThrowsException(status = HttpStatus.CONFLICT, exception = ChargeDefinitionIsReadOnly.class)
void changeChargeDefinition(
@PathVariable("productidentifier") final String productIdentifier,
@PathVariable("chargedefinitionidentifier") final String chargeDefinitionIdentifier,
@@ -280,7 +271,6 @@
produces = MediaType.ALL_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
- @ThrowsException(status = HttpStatus.CONFLICT, exception = ChargeDefinitionIsReadOnly.class)
void deleteChargeDefinition(
@PathVariable("productidentifier") final String productIdentifier,
@PathVariable("chargedefinitionidentifier") final String chargeDefinitionIdentifier);
@@ -345,7 +335,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 +350,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 +365,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 +378,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);
@@ -462,6 +452,4 @@
CasePage getAllCases(
@RequestParam("pageIndex") final Integer pageIndex,
@RequestParam("size") final Integer size);
-
- //TODO: find a way to list cases by customer even though the portfolio contains products which may be associated with groups instead of customers.
}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Pattern.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Pattern.java
index 66e7314..55381ce 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Pattern.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Pattern.java
@@ -15,6 +15,7 @@
*/
package io.mifos.portfolio.api.v1.domain;
+import java.util.Objects;
import java.util.Set;
/**
@@ -23,16 +24,12 @@
@SuppressWarnings({"WeakerAccess", "unused"})
public class Pattern {
private String parameterPackage;
- private Set<String> accountAssignmentsRequired;
+ private Set<String> accountAssignmentGroups;
+ private Set<RequiredAccountAssignment> accountAssignmentsRequired;
public Pattern() {
}
- public Pattern(String parametersNameSpace, Set<String> accountAssignmentsRequired) {
- this.parameterPackage = parametersNameSpace;
- this.accountAssignmentsRequired = accountAssignmentsRequired;
- }
-
public String getParameterPackage() {
return parameterPackage;
}
@@ -41,11 +38,19 @@
this.parameterPackage = parameterPackage;
}
- public Set<String> getAccountAssignmentsRequired() {
+ public Set<String> getAccountAssignmentGroups() {
+ return accountAssignmentGroups;
+ }
+
+ public void setAccountAssignmentGroups(Set<String> accountAssignmentGroups) {
+ this.accountAssignmentGroups = accountAssignmentGroups;
+ }
+
+ public Set<RequiredAccountAssignment> getAccountAssignmentsRequired() {
return accountAssignmentsRequired;
}
- public void setAccountAssignmentsRequired(Set<String> accountAssignmentsRequired) {
+ public void setAccountAssignmentsRequired(Set<RequiredAccountAssignment> accountAssignmentsRequired) {
this.accountAssignmentsRequired = accountAssignmentsRequired;
}
@@ -53,25 +58,23 @@
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
-
Pattern pattern = (Pattern) o;
-
- return parameterPackage != null ? parameterPackage.equals(pattern.parameterPackage) : pattern.parameterPackage == null && (accountAssignmentsRequired != null ? accountAssignmentsRequired.equals(pattern.accountAssignmentsRequired) : pattern.accountAssignmentsRequired == null);
-
+ return Objects.equals(parameterPackage, pattern.parameterPackage) &&
+ Objects.equals(accountAssignmentGroups, pattern.accountAssignmentGroups) &&
+ Objects.equals(accountAssignmentsRequired, pattern.accountAssignmentsRequired);
}
@Override
public int hashCode() {
- int result = parameterPackage != null ? parameterPackage.hashCode() : 0;
- result = 31 * result + (accountAssignmentsRequired != null ? accountAssignmentsRequired.hashCode() : 0);
- return result;
+ return Objects.hash(parameterPackage, accountAssignmentGroups, accountAssignmentsRequired);
}
@Override
public String toString() {
return "Pattern{" +
- "parameterPackage='" + parameterPackage + '\'' +
- ", accountAssignmentsRequired=" + accountAssignmentsRequired +
- '}';
+ "parameterPackage='" + parameterPackage + '\'' +
+ ", accountAssignmentGroups=" + accountAssignmentGroups +
+ ", accountAssignmentsRequired=" + accountAssignmentsRequired +
+ '}';
}
}
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/main/java/io/mifos/portfolio/api/v1/domain/RequiredAccountAssignment.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/RequiredAccountAssignment.java
new file mode 100644
index 0000000..1cea505
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/RequiredAccountAssignment.java
@@ -0,0 +1,86 @@
+/*
+ * 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.portfolio.api.v1.domain;
+
+import javax.annotation.Nullable;
+import java.util.Objects;
+
+@SuppressWarnings("unused")
+public class RequiredAccountAssignment {
+ private String accountDesignator;
+ private String accountType;
+ private @Nullable String group;
+
+ public RequiredAccountAssignment(String accountDesignator, String accountType) {
+ this.accountDesignator = accountDesignator;
+ this.accountType = accountType;
+ this.group = null;
+ }
+
+ public RequiredAccountAssignment(String accountDesignator, String accountType, String ledger) {
+ this.accountDesignator = accountDesignator;
+ this.accountType = accountType;
+ this.group = ledger;
+ }
+
+ public String getAccountDesignator() {
+ return accountDesignator;
+ }
+
+ public void setAccountDesignator(String accountDesignator) {
+ this.accountDesignator = accountDesignator;
+ }
+
+ public String getAccountType() {
+ return accountType;
+ }
+
+ public void setAccountType(String thothType) {
+ this.accountType = thothType;
+ }
+
+ public String getGroup() {
+ return group;
+ }
+
+ public void setGroup(String group) {
+ this.group = group;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RequiredAccountAssignment that = (RequiredAccountAssignment) o;
+ return Objects.equals(accountDesignator, that.accountDesignator) &&
+ Objects.equals(accountType, that.accountType) &&
+ Objects.equals(group, that.group);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(accountDesignator, accountType, group);
+ }
+
+ @Override
+ public String toString() {
+ return "RequiredAccountAssignment{" +
+ "accountDesignator='" + accountDesignator + '\'' +
+ ", accountType='" + accountType + '\'' +
+ ", group='" + group + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidLossProvisionList.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidLossProvisionList.java
new file mode 100644
index 0000000..3085033
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidLossProvisionList.java
@@ -0,0 +1,44 @@
+/*
+ * 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.portfolio.api.v1.validation;
+
+import io.mifos.individuallending.api.v1.domain.product.LossProvisionStep;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+public class CheckValidLossProvisionList implements ConstraintValidator<ValidLossProvisionList, List<LossProvisionStep>> {
+ @Override
+ public void initialize(ValidLossProvisionList constraintAnnotation) {
+
+ }
+
+ @Override
+ public boolean isValid(
+ final List<LossProvisionStep> value,
+ final ConstraintValidatorContext context) {
+ final BigDecimal sum = value.stream()
+ .map(LossProvisionStep::getPercentProvision)
+ .map(x -> x.setScale(2, BigDecimal.ROUND_HALF_EVEN))
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ return sum.compareTo(BigDecimal.valueOf(100_00, 2)) == 0;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidLossProvisionList.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidLossProvisionList.java
new file mode 100644
index 0000000..4e4fb44
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidLossProvisionList.java
@@ -0,0 +1,38 @@
+/*
+ * 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.portfolio.api.v1.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+ validatedBy = {CheckValidLossProvisionList.class}
+)
+public @interface ValidLossProvisionList {
+ String message() default "Loss provision percents should sum to 100.";
+
+ Class<?>[] groups() default {};
+
+ Class<? extends Payload>[] payload() default {};
+}
diff --git a/api/src/test/java/io/mifos/Fixture.java b/api/src/test/java/io/mifos/Fixture.java
index 5643ecb..6e10700 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"));
@@ -70,11 +63,11 @@
accountAssignments.add(new AccountAssignment(INTEREST_ACCRUAL, "001-007"));
accountAssignments.add(new AccountAssignment(LATE_FEE_INCOME, "001-008"));
accountAssignments.add(new AccountAssignment(LATE_FEE_ACCRUAL, "001-009"));
- accountAssignments.add(new AccountAssignment(ARREARS_ALLOWANCE, "001-010"));
+ accountAssignments.add(new AccountAssignment(GENERAL_LOSS_ALLOWANCE, "001-010"));
//accountAssignments.add(new AccountAssignment(ENTRY, ...));
// Don't assign entry account in test since it usually will not be assigned IRL.
accountAssignments.add(new AccountAssignment(LOAN_FUNDS_SOURCE, LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER));
- accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN, "001-013"));
+ accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN_GROUP, "001-013"));
product.setAccountAssignments(accountAssignments);
final ProductParameters productParameters = new ProductParameters();
@@ -111,7 +104,7 @@
final Set<AccountAssignment> accountAssignments = new HashSet<>();
- accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN, "001-011"));
+ accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN_GROUP, "001-011"));
accountAssignments.add(new AccountAssignment(ENTRY, "001-012"));
ret.setAccountAssignments(accountAssignments);
ret.setCurrentState(Case.State.CREATED.name());
diff --git a/api/src/test/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionConfigurationTest.java b/api/src/test/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionConfigurationTest.java
new file mode 100644
index 0000000..ba6e24b
--- /dev/null
+++ b/api/src/test/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionConfigurationTest.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.api.v1.domain.product;
+
+import io.mifos.core.test.domain.ValidationTest;
+import io.mifos.core.test.domain.ValidationTestCase;
+import org.junit.runners.Parameterized;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class LossProvisionConfigurationTest extends ValidationTest<LossProvisionConfiguration> {
+
+ public LossProvisionConfigurationTest(ValidationTestCase<LossProvisionConfiguration> testCase) {
+ super(testCase);
+ }
+
+ @Override
+ protected LossProvisionConfiguration createValidTestSubject() {
+ final LossProvisionConfiguration ret = new LossProvisionConfiguration();
+ final List<LossProvisionStep> lossProvisionSteps = new ArrayList<>();
+ lossProvisionSteps.add(new LossProvisionStep(0, BigDecimal.ONE));
+ lossProvisionSteps.add(new LossProvisionStep(1, BigDecimal.valueOf(9)));
+ lossProvisionSteps.add(new LossProvisionStep(10, BigDecimal.valueOf(20)));
+ lossProvisionSteps.add(new LossProvisionStep(50, BigDecimal.valueOf(70)));
+ ret.setLossProvisionSteps(lossProvisionSteps);
+ return ret;
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<ValidationTestCase> ret = new ArrayList<>();
+
+ ret.add(new ValidationTestCase<LossProvisionConfiguration>("valid"));
+ ret.add(new ValidationTestCase<LossProvisionConfiguration>("emptyList")
+ .adjustment(x -> x.setLossProvisionSteps(Collections.emptyList()))
+ .valid(false));
+ ret.add(new ValidationTestCase<LossProvisionConfiguration>("nullList")
+ .adjustment(x -> x.setLossProvisionSteps(Collections.emptyList()))
+ .valid(false));
+ ret.add(new ValidationTestCase<LossProvisionConfiguration>("sumTooSmall")
+ .adjustment(x -> x.getLossProvisionSteps().get(0).setPercentProvision(BigDecimal.valueOf(0.1)))
+ .valid(false));
+ ret.add(new ValidationTestCase<LossProvisionConfiguration>("sumTooLarge")
+ .adjustment(x -> x.getLossProvisionSteps().get(3).setPercentProvision(BigDecimal.valueOf(71)))
+ .valid(false));
+
+ return ret;
+ }
+}
\ No newline at end of file
diff --git a/api/src/test/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionStepTest.java b/api/src/test/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionStepTest.java
new file mode 100644
index 0000000..e401491
--- /dev/null
+++ b/api/src/test/java/io/mifos/individuallending/api/v1/domain/product/LossProvisionStepTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.product;
+
+import io.mifos.core.test.domain.ValidationTest;
+import io.mifos.core.test.domain.ValidationTestCase;
+import org.junit.runners.Parameterized;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class LossProvisionStepTest extends ValidationTest<LossProvisionStep> {
+
+ public LossProvisionStepTest(final ValidationTestCase<LossProvisionStep> testCase)
+ {
+ super(testCase);
+ }
+
+ @Override
+ protected LossProvisionStep createValidTestSubject() {
+ final LossProvisionStep ret = new LossProvisionStep();
+ ret.setPercentProvision(BigDecimal.ONE);
+ ret.setDaysLate(10);
+ return ret;
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<ValidationTestCase> ret = new ArrayList<>();
+
+ ret.add(new ValidationTestCase<LossProvisionStep>("valid"));
+ ret.add(new ValidationTestCase<LossProvisionStep>("largeDaysLate")
+ .adjustment(x -> x.setDaysLate(Integer.MAX_VALUE))
+ .valid(true));
+ ret.add(new ValidationTestCase<LossProvisionStep>("zeroDaysLate")
+ .adjustment(x -> x.setDaysLate(0))
+ .valid(true));
+ ret.add(new ValidationTestCase<LossProvisionStep>("oneDaysLate")
+ .adjustment(x -> x.setDaysLate(1))
+ .valid(true));
+ ret.add(new ValidationTestCase<LossProvisionStep>("negativeDaysLate")
+ .adjustment(x -> x.setDaysLate(-1))
+ .valid(false));
+ ret.add(new ValidationTestCase<LossProvisionStep>("negativeProvisioning")
+ .adjustment(x -> x.setPercentProvision(BigDecimal.TEN.negate()))
+ .valid(false));
+ ret.add(new ValidationTestCase<LossProvisionStep>("over100Provisioning")
+ .adjustment(x -> x.setPercentProvision(BigDecimal.valueOf(100_01, 2)))
+ .valid(false));
+ ret.add(new ValidationTestCase<LossProvisionStep>("exactly100Provisioning")
+ .adjustment(x -> x.setPercentProvision(BigDecimal.valueOf(100_00, 2)))
+ .valid(true));
+
+ return ret;
+ }
+
+}
\ No newline at end of file
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..8d46e36 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -31,6 +31,7 @@
import io.mifos.portfolio.api.v1.domain.*;
import io.mifos.portfolio.api.v1.events.*;
import io.mifos.portfolio.service.config.PortfolioServiceConfiguration;
+import io.mifos.portfolio.service.internal.util.AccountingListener;
import io.mifos.portfolio.service.internal.util.RhythmAdapter;
import org.junit.*;
import org.junit.runner.RunWith;
@@ -41,6 +42,7 @@
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
@@ -124,6 +126,9 @@
@MockBean
CustomerManager customerManager;
+ @SpyBean
+ AccountingListener accountingListener;
+
@SuppressWarnings("SpringAutowiredFieldsWarningInspection")
@Autowired
@Qualifier(LOGGER_NAME)
@@ -132,7 +137,7 @@
@Before
public void prepTest() {
userContext = this.tenantApplicationSecurityEnvironment.createAutoUserContext(TEST_USER);
- AccountingFixture.mockAccountingPrereqs(ledgerManager);
+ AccountingFixture.mockAccountingPrereqs(ledgerManager, accountingListener);
Mockito.doReturn(true).when(customerManager).isCustomerInGoodStanding(Fixture.CUSTOMER_IDENTIFIER);
}
@@ -262,24 +267,31 @@
Assert.assertEquals(actionList, portfolioManager.getActionsForCase(productIdentifier, customerCaseIdentifier));
}
- void checkCostComponentForActionCorrect(final String productIdentifier,
- final String customerCaseIdentifier,
- final Action action,
- final Set<String> accountDesignators,
- final BigDecimal amount,
- final CostComponent... expectedCostComponents) {
- final List<CostComponent> costComponents = portfolioManager.getCostComponentsForAction(
+ Payment checkCostComponentForActionCorrect(
+ final String productIdentifier,
+ final String customerCaseIdentifier,
+ final Action action,
+ final Set<String> accountDesignators,
+ final BigDecimal amount,
+ final LocalDateTime forDateTime,
+ final int minorCurrencyUnits,
+ final CostComponent... expectedCostComponents) {
+ final Payment payment = portfolioManager.getCostComponentsForAction(
productIdentifier,
customerCaseIdentifier,
action.name(),
accountDesignators,
- amount
+ amount,
+ DateConverter.toIsoString(forDateTime)
);
- final Set<CostComponent> setOfCostComponents = new HashSet<>(costComponents);
+ payment.getCostComponents().forEach(x -> x.setAmount(x.getAmount().setScale(minorCurrencyUnits, BigDecimal.ROUND_HALF_EVEN)));
+ 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());
Assert.assertEquals(setOfExpectedCostComponents, setOfCostComponents);
+
+ return payment;
}
void setFeeToFixedValue(final String productIdentifier,
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..892a27d 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -19,8 +19,11 @@
import io.mifos.accounting.api.v1.domain.*;
import io.mifos.core.api.util.NotFoundException;
import io.mifos.core.lang.DateConverter;
+import io.mifos.core.lang.TenantContextHolder;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.portfolio.service.internal.util.AccountingListener;
import org.hamcrest.Description;
+import org.junit.Assert;
import org.mockito.AdditionalMatchers;
import org.mockito.ArgumentMatcher;
import org.mockito.Matchers;
@@ -50,7 +53,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,10 +63,11 @@
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";
+ static final String PRODUCT_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER = "7353.0";
+ static final String GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER = "3010";
+ static final String GENERAL_EXPENSE_ACCOUNT_IDENTIFIER = "3011";
static final Map<String, AccountData> accountMap = new HashMap<>();
@@ -102,6 +105,15 @@
.fetchAccountEntriesStream(Mockito.eq(account.getIdentifier()), Matchers.anyString(), Matchers.anyString(), AdditionalMatchers.or(Matchers.eq("DESC"), Matchers.eq("ASC")));
}
+ private static void makeLedgerResponsive(
+ final Ledger ledger,
+ final LedgerManager ledgerManagerMock)
+ {
+ Mockito.doReturn(ledger).when(ledgerManagerMock).findLedger(ledger.getIdentifier());
+ Mockito.doReturn(emptyAccountsPage()).when(ledgerManagerMock).fetchAccountsOfLedger(Mockito.eq(ledger.getIdentifier()),
+ Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
+ }
+
private static Ledger cashLedger() {
final Ledger ret = new Ledger();
@@ -129,15 +141,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,19 +226,11 @@
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);
ret.setLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
- ret.setType(AccountType.LIABILITY.name()); //TODO: ??
+ ret.setType(AccountType.REVENUE.name());
return ret;
}
@@ -243,15 +238,28 @@
final Account ret = new Account();
ret.setIdentifier(LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER);
ret.setLedger(ACCRUED_INCOME_LEDGER_IDENTIFIER);
- ret.setType(AccountType.LIABILITY.name()); //TODO: ??
+ ret.setType(AccountType.REVENUE.name());
return ret;
}
- private static Account arrearsAllowanceAccount() {
+ private static Account productLossAllowanceAccount() {
final Account ret = new Account();
- ret.setIdentifier(ARREARS_ALLOWANCE_ACCOUNT_IDENTIFIER);
- //ret.setLedger(LOAN_INCOME_LEDGER_IDENTIFIER); //TODO: ??
- ret.setType(AccountType.LIABILITY.name()); //TODO: ??
+ ret.setIdentifier(PRODUCT_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER);
+ ret.setType(AccountType.ASSET.name());
+ return ret;
+ }
+
+ private static Account generalLossAllowanceAccount() {
+ final Account ret = new Account();
+ ret.setIdentifier(GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER);
+ ret.setType(AccountType.EXPENSE.name());
+ return ret;
+ }
+
+ private static Account generalExpenseAccount() {
+ final Account ret = new Account();
+ ret.setIdentifier(GENERAL_EXPENSE_ACCOUNT_IDENTIFIER);
+ ret.setType(AccountType.EXPENSE.name());
return ret;
}
@@ -270,20 +278,11 @@
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");
-
+ private static AccountPage emptyAccountsPage() {
final AccountPage ret = new AccountPage();
- ret.setTotalElements(3L);
+ ret.setTotalElements(0L);
ret.setTotalPages(1);
- ret.setAccounts(Arrays.asList(pendingDisbursalAccount1, pendingDisbursalAccount2, pendingDisbursalAccount3));
+ ret.setAccounts(Collections.emptyList());
return ret;
}
@@ -305,11 +304,16 @@
private static class AccountMatcher extends ArgumentMatcher<Account> {
private final String ledgerIdentifer;
+ private final String accountDesignator;
private final AccountType type;
private Account matchedArgument;
- private AccountMatcher(final String ledgerIdentifier, final AccountType type) {
+ private AccountMatcher(
+ final String ledgerIdentifier,
+ final String accountDesignator,
+ final AccountType type) {
this.ledgerIdentifer = ledgerIdentifier;
+ this.accountDesignator = accountDesignator;
this.type = type;
this.matchedArgument = null; //Set when matches called and returns true.
}
@@ -324,6 +328,7 @@
final Account checkedArgument = (Account) argument;
final boolean ret = checkedArgument.getLedger().equals(ledgerIdentifer) &&
+ checkedArgument.getIdentifier().contains(accountDesignator) &&
checkedArgument.getType().equals(type.name()) &&
checkedArgument.getBalance() == 0.0;
@@ -333,9 +338,75 @@
return ret;
}
+ @Override
+ public void describeTo(final Description description) {
+ description.appendText(this.toString());
+ }
+
Account getMatchedArgument() {
return matchedArgument;
}
+
+ @Override
+ public String toString() {
+ return "AccountMatcher{" +
+ "ledgerIdentifer='" + ledgerIdentifer + '\'' +
+ ", accountDesignator='" + accountDesignator + '\'' +
+ ", type=" + type +
+ '}';
+ }
+ }
+
+ private static class LedgerMatcher extends ArgumentMatcher<Ledger> {
+ private final String ledgerIdentifer;
+ private final AccountType type;
+ private Ledger matchedArgument;
+
+ LedgerMatcher(String ledgerIdentifier, AccountType type) {
+ this.ledgerIdentifer = ledgerIdentifier;
+ this.type = type;
+ this.matchedArgument = null; //Set when matches called and returns true.
+ }
+
+ @Override
+ public boolean matches(final Object argument) {
+ if (argument == null)
+ return false;
+ if (! (argument instanceof Ledger))
+ return false;
+
+ final Ledger checkedArgument = (Ledger) argument;
+
+ final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+ final Set errors = validator.validate(checkedArgument);
+
+ Assert.assertEquals(0, errors.size());
+
+ final boolean ret = checkedArgument.getParentLedgerIdentifier().equals(ledgerIdentifer) &&
+ checkedArgument.getType().equals(type.name());
+
+ if (ret)
+ matchedArgument = checkedArgument;
+
+ return ret;
+ }
+
+ @Override
+ public void describeTo(final Description description) {
+ description.appendText(this.toString());
+ }
+
+ Ledger getMatchedArgument() {
+ return matchedArgument;
+ }
+
+ @Override
+ public String toString() {
+ return "LedgerMatcher{" +
+ "ledgerIdentifer='" + ledgerIdentifer + '\'' +
+ ", type=" + type +
+ '}';
+ }
}
private static class JournalEntryMatcher extends ArgumentMatcher<JournalEntry> {
@@ -397,6 +468,22 @@
journalEntry.getMessage(),
journalEntry.getTransactionDate(),
Double.valueOf(debtor.getAmount())));
+
+ final BigDecimal creditorSum = journalEntry.getCreditors().stream()
+ .map(Creditor::getAmount)
+ .map(Double::valueOf)
+ .map(BigDecimal::valueOf)
+ .map(x -> x.setScale(4, BigDecimal.ROUND_HALF_EVEN))
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+ final BigDecimal debtorSum = journalEntry.getDebtors().stream()
+ .map(Debtor::getAmount)
+ .map(Double::valueOf)
+ .map(BigDecimal::valueOf)
+ .map(x -> x.setScale(4, BigDecimal.ROUND_HALF_EVEN))
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+ Assert.assertEquals(creditorSum, debtorSum);
return null;
}
}
@@ -422,6 +509,22 @@
}
}
+ private static class CreateLedgerAnswer implements Answer {
+ private final AccountingListener accountingListener;
+
+ CreateLedgerAnswer(AccountingListener accountingListener) {
+ this.accountingListener = accountingListener;
+ }
+
+ @Override
+ public Void answer(final InvocationOnMock invocation) throws Throwable {
+ final Ledger ledger = invocation.getArgumentAt(1, Ledger.class);
+ makeLedgerResponsive(ledger, (LedgerManager) invocation.getMock());
+ accountingListener.onPostLedger(TenantContextHolder.checkedGetIdentifier(), ledger.getIdentifier());
+ return null;
+ }
+ }
+
static class AccountEntriesStreamAnswer implements Answer {
private final AccountData accountData;
@@ -449,7 +552,7 @@
}
}
- static void mockAccountingPrereqs(final LedgerManager ledgerManagerMock) {
+ static void mockAccountingPrereqs(final LedgerManager ledgerManagerMock, final AccountingListener accountingListener) {
makeAccountResponsive(loanFundsSourceAccount(), universalCreationDate, ledgerManagerMock);
makeAccountResponsive(loanOriginationFeesIncomeAccount(), universalCreationDate, ledgerManagerMock);
makeAccountResponsive(processingFeeIncomeAccount(), universalCreationDate, ledgerManagerMock);
@@ -457,52 +560,49 @@
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);
+ makeAccountResponsive(productLossAllowanceAccount(), universalCreationDate, ledgerManagerMock);
+ makeAccountResponsive(generalLossAllowanceAccount(), universalCreationDate, ledgerManagerMock);
+ makeAccountResponsive(generalExpenseAccount(), universalCreationDate, ledgerManagerMock);
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());
Mockito.doAnswer(new CreateJournalEntryAnswer()).when(ledgerManagerMock).createJournalEntry(Matchers.any(JournalEntry.class));
+ Mockito.doAnswer(new CreateLedgerAnswer(accountingListener)).when(ledgerManagerMock).addSubLedger(Matchers.anyString(), Matchers.any(Ledger.class));
}
static void mockBalance(final String accountIdentifier, final BigDecimal balance) {
accountMap.get(accountIdentifier).setBalance(balance.doubleValue());
}
- static String verifyAccountCreation(final LedgerManager ledgerManager,
- final String ledgerIdentifier,
- final AccountType type) {
- final AccountMatcher specifiesCorrectAccount = new AccountMatcher(ledgerIdentifier, type);
+ static String verifyAccountCreationMatchingDesignator(
+ final LedgerManager ledgerManager,
+ final String ledgerIdentifier,
+ final String accountDesignator,
+ final AccountType type) {
+ final AccountMatcher specifiesCorrectAccount = new AccountMatcher(ledgerIdentifier, accountDesignator, type);
Mockito.verify(ledgerManager).createAccount(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectAccount)));
return specifiesCorrectAccount.getMatchedArgument().getIdentifier();
}
- 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 String verifyLedgerCreation(
+ final LedgerManager ledgerManager,
+ final String ledgerIdentifier,
+ final AccountType type) {
+ final LedgerMatcher specifiesCorrectLedger = new LedgerMatcher(ledgerIdentifier, type);
+ Mockito.verify(ledgerManager).addSubLedger(Matchers.anyString(), AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectLedger)));
+ makeLedgerResponsive(specifiesCorrectLedger.getMatchedArgument(), ledgerManager);
+ return specifiesCorrectLedger.getMatchedArgument().getIdentifier();
}
static void verifyTransfer(final LedgerManager ledgerManager,
@@ -511,10 +611,21 @@
final String productIdentifier,
final String caseIdentifier,
final Action action) {
- final Set<Debtor> filteredDebtors = debtors.stream().filter(x -> BigDecimal.valueOf(Double.valueOf(x.getAmount())).compareTo(BigDecimal.ZERO) != 0).collect(Collectors.toSet());
- final Set<Creditor> filteredCreditors = creditors.stream().filter(x -> BigDecimal.valueOf(Double.valueOf(x.getAmount())).compareTo(BigDecimal.ZERO) != 0).collect(Collectors.toSet());
- final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(filteredDebtors, filteredCreditors, productIdentifier, caseIdentifier, action);
- Mockito.verify(ledgerManager, Mockito.atLeastOnce()).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
-
+ final Set<Debtor> filteredDebtors = debtors.stream()
+ .filter(x -> BigDecimal.valueOf(Double.valueOf(x.getAmount())).compareTo(BigDecimal.ZERO) != 0)
+ .collect(Collectors.toSet());
+ final Set<Creditor> filteredCreditors = creditors.stream()
+ .filter(x -> BigDecimal.valueOf(Double.valueOf(x.getAmount())).compareTo(BigDecimal.ZERO) != 0)
+ .collect(Collectors.toSet());
+ if (filteredCreditors.size() == 0 && filteredDebtors.size() == 0)
+ return;
+ final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(
+ filteredDebtors,
+ filteredCreditors,
+ productIdentifier,
+ caseIdentifier,
+ action);
+ Mockito.verify(ledgerManager, Mockito.atLeastOnce())
+ .createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
}
}
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/portfolio/Fixture.java b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
index de8fc57..092052b 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -19,11 +19,11 @@
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.caseinstance.CreditWorthinessFactor;
import io.mifos.individuallending.api.v1.domain.caseinstance.CreditWorthinessSnapshot;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.product.ProductParameters;
import io.mifos.portfolio.api.v1.domain.*;
import java.math.BigDecimal;
-import java.math.RoundingMode;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Consumer;
@@ -39,7 +39,7 @@
@SuppressWarnings({"WeakerAccess", "unused"})
public class Fixture {
static final int MINOR_CURRENCY_UNIT_DIGITS = 2;
- static final BigDecimal INTEREST_RATE = BigDecimal.valueOf(0.10).setScale(4, RoundingMode.HALF_EVEN);
+ static final BigDecimal INTEREST_RATE = BigDecimal.valueOf(10_00, 2);
static final BigDecimal ACCRUAL_PERIODS = BigDecimal.valueOf(365.2425);
public static final String CUSTOMER_IDENTIFIER = "alice";
@@ -60,26 +60,33 @@
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));
+ accountAssignments.add(new AccountAssignment(PRODUCT_LOSS_ALLOWANCE, PRODUCT_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER));
+ accountAssignments.add(new AccountAssignment(GENERAL_LOSS_ALLOWANCE, GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER));
+ accountAssignments.add(new AccountAssignment(GENERAL_EXPENSE, GENERAL_EXPENSE_ACCOUNT_IDENTIFIER));
//accountAssignments.add(new AccountAssignment(ENTRY, ...));
// Don't assign entry account in test since it usually will not be assigned IRL.
accountAssignments.add(new AccountAssignment(LOAN_FUNDS_SOURCE, LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER));
- final AccountAssignment customerLoanAccountAssignment = new AccountAssignment();
- customerLoanAccountAssignment.setDesignator(CUSTOMER_LOAN);
- customerLoanAccountAssignment.setLedgerIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
- accountAssignments.add(customerLoanAccountAssignment);
+ final AccountAssignment customerLoanPrincipalAccountAssignment = new AccountAssignment();
+ customerLoanPrincipalAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ customerLoanPrincipalAccountAssignment.setLedgerIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
+ accountAssignments.add(customerLoanPrincipalAccountAssignment);
+
+ final AccountAssignment customerLoanInterestAccountAssignment = new AccountAssignment();
+ customerLoanInterestAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_INTEREST);
+ customerLoanInterestAccountAssignment.setLedgerIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
+ accountAssignments.add(customerLoanInterestAccountAssignment);
+
+ final AccountAssignment customerLoanFeesAccountAssignment = new AccountAssignment();
+ customerLoanFeesAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_FEES);
+ customerLoanFeesAccountAssignment.setLedgerIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
+ accountAssignments.add(customerLoanFeesAccountAssignment);
product.setAccountAssignments(accountAssignments);
final ProductParameters productParameters = new ProductParameters();
@@ -118,7 +125,7 @@
final Set<AccountAssignment> accountAssignments = new HashSet<>();
ret.setAccountAssignments(accountAssignments);
ret.setCurrentState(Case.State.CREATED.name());
- ret.setInterest(BigDecimal.valueOf(10_00, 2));
+ ret.setInterest(INTEREST_RATE);
final CaseParameters caseParameters = getTestCaseParameters();
final Gson gson = new Gson();
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..63f71b3 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -22,6 +22,7 @@
import io.mifos.core.api.util.ApiFactory;
import io.mifos.core.lang.DateConverter;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
+import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
@@ -35,6 +36,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;
@@ -45,6 +47,7 @@
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
+import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static io.mifos.portfolio.Fixture.MINOR_CURRENCY_UNIT_DIGITS;
@@ -53,8 +56,8 @@
* @author Myrle Krantz
*/
public class TestAccountingInteractionInLoanWorkflow extends AbstractPortfolioTest {
- private static final BigDecimal PROCESSING_FEE_AMOUNT = BigDecimal.valueOf(100_00, MINOR_CURRENCY_UNIT_DIGITS);
- private static final BigDecimal LOAN_ORIGINATION_FEE_AMOUNT = BigDecimal.valueOf(100_00, MINOR_CURRENCY_UNIT_DIGITS);
+ private static final BigDecimal PROCESSING_FEE_AMOUNT = BigDecimal.valueOf(50_00, MINOR_CURRENCY_UNIT_DIGITS);
+ private static final BigDecimal LOAN_ORIGINATION_FEE_AMOUNT = BigDecimal.valueOf(50_00, MINOR_CURRENCY_UNIT_DIGITS);
private static final BigDecimal DISBURSEMENT_FEE_LOWER_RANGE_AMOUNT = BigDecimal.valueOf(10_00, MINOR_CURRENCY_UNIT_DIGITS);
private static final BigDecimal DISBURSEMENT_FEE_UPPER_RANGE_AMOUNT = BigDecimal.valueOf(1_00, MINOR_CURRENCY_UNIT_DIGITS);
private static final String DISBURSEMENT_RANGES = "disbursement_ranges";
@@ -67,12 +70,14 @@
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 String customerLoanPrincipalIdentifier = null;
+ private String customerLoanInterestIdentifier = null;
+ private String customerLoanFeeIdentifier = null;
- private BigDecimal expectedCurrentBalance = null;
+ private BigDecimal expectedCurrentPrincipal = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
private BigDecimal interestAccrued = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
+ private BigDecimal nonLateFees = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
+ private BigDecimal lateFees = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
@Before
@@ -82,10 +87,48 @@
@Test
public void workflowTerminatingInApplicationDenial() throws InterruptedException {
+ final LocalDateTime today = midnightToday();
step1CreateProduct();
step2CreateCase();
- step3OpenCase();
- step4DenyCase();
+ step3OpenCase(today);
+ step4DenyCase(today);
+ }
+
+ @Test
+ public void cantChangeDeniedCase() throws InterruptedException {
+ final LocalDateTime today = midnightToday();
+ step1CreateProduct();
+ step2CreateCase();
+ step3OpenCase(today);
+ step4DenyCase(today);
+
+ try {
+ customerCase.setInterest(BigDecimal.ONE);
+ portfolioManager.changeCase(product.getIdentifier(), customerCase.getIdentifier(), customerCase);
+ Assert.fail("Changing a denied case should fail.");
+ }
+ catch (IllegalArgumentException ignored) {
+
+ }
+ }
+
+ @Test
+ public void cantChangeApprovedCase() throws InterruptedException {
+ final LocalDateTime today = midnightToday();
+
+ step1CreateProduct();
+ step2CreateCase();
+ step3OpenCase(today);
+ step4ApproveCase(today);
+
+ try {
+ customerCase.setInterest(BigDecimal.ONE);
+ portfolioManager.changeCase(product.getIdentifier(), customerCase.getIdentifier(), customerCase);
+ Assert.fail("Changing a denied case should fail.");
+ }
+ catch (IllegalArgumentException ignored) {
+
+ }
}
@Test
@@ -94,14 +137,18 @@
step1CreateProduct();
step2CreateCase();
- step3OpenCase();
- step4ApproveCase();
+ step3OpenCase(today);
+ step4ApproveCase(today);
step5Disburse(
BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
+ today,
UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
- step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), null);
- step7PaybackPartialAmount(expectedCurrentBalance, today, 0, BigDecimal.ZERO);
- step8Close();
+ step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), BigDecimal.ZERO);
+ step7PaybackPartialAmount(
+ expectedCurrentPrincipal.add(nonLateFees).add(interestAccrued),
+ today,
+ BigDecimal.ZERO);
+ step8Close(today);
}
@Test
@@ -110,17 +157,22 @@
step1CreateProduct();
step2CreateCase();
- step3OpenCase();
- step4ApproveCase();
+ step3OpenCase(today);
+ step4ApproveCase(today);
step5Disburse(
BigDecimal.valueOf(500_00, MINOR_CURRENCY_UNIT_DIGITS),
+ today,
ChargeIdentifiers.DISBURSEMENT_FEE_ID, BigDecimal.valueOf(10_00, MINOR_CURRENCY_UNIT_DIGITS));
step5Disburse(
BigDecimal.valueOf(1_500_00, MINOR_CURRENCY_UNIT_DIGITS),
+ today,
UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(15_00, MINOR_CURRENCY_UNIT_DIGITS));
- step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), null);
- step7PaybackPartialAmount(expectedCurrentBalance, today, 0, BigDecimal.ZERO);
- step8Close();
+ step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), BigDecimal.ZERO);
+ step7PaybackPartialAmount(
+ expectedCurrentPrincipal.add(nonLateFees).add(interestAccrued),
+ today,
+ BigDecimal.ZERO);
+ step8Close(today);
}
@Test
@@ -129,29 +181,33 @@
step1CreateProduct();
step2CreateCase();
- step3OpenCase();
- step4ApproveCase();
+ step3OpenCase(today);
+ step4ApproveCase(today);
step5Disburse(
BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
+ today,
UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
- step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), null);
- final BigDecimal repayment1 = expectedCurrentBalance.divide(BigDecimal.valueOf(2), BigDecimal.ROUND_HALF_EVEN);
+ step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), BigDecimal.ZERO);
+ final BigDecimal repayment1 = expectedCurrentPrincipal.divide(BigDecimal.valueOf(2), BigDecimal.ROUND_HALF_EVEN);
step7PaybackPartialAmount(
repayment1.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN),
today,
- 0, BigDecimal.ZERO);
- step7PaybackPartialAmount(expectedCurrentBalance, today, 0, BigDecimal.ZERO);
- step8Close();
+ BigDecimal.ZERO);
+ step7PaybackPartialAmount(expectedCurrentPrincipal, today, BigDecimal.ZERO);
+ step8Close(today);
}
@Test
public void workflowWithNegativePaymentSize() throws InterruptedException {
+ final LocalDateTime today = midnightToday();
+
step1CreateProduct();
step2CreateCase();
- step3OpenCase();
- step4ApproveCase();
+ step3OpenCase(today);
+ step4ApproveCase(today);
try {
step5Disburse(BigDecimal.valueOf(-2).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN),
+ today,
UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
Assert.fail("Expected an IllegalArgumentException.");
}
@@ -164,31 +220,38 @@
step1CreateProduct();
step2CreateCase();
- step3OpenCase();
- step4ApproveCase();
+ step3OpenCase(today);
+ step4ApproveCase(today);
+
+ final List<PlannedPayment> plannedPayments = individualLending.getPaymentScheduleForCaseStream(
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ null)
+ .collect(Collectors.toList());
+
step5Disburse(
BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
+ today,
UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
int week = 0;
- final List<BigDecimal> repayments = new ArrayList<>();
- while (expectedCurrentBalance.compareTo(BigDecimal.ZERO) > 0) {
- logger.info("Simulating week {}. Expected current balance {}.", week, expectedCurrentBalance);
+ final List<Payment> payments = new ArrayList<>();
+ while (expectedCurrentPrincipal.compareTo(BigDecimal.ZERO) > 0) {
+ logger.info("Simulating week {}. Expected current principal {}.", week, expectedCurrentPrincipal);
step6CalculateInterestAndCheckForLatenessForWeek(today, week);
- final BigDecimal nextRepaymentAmount = findNextRepaymentAmount(today, (week+1)*7);
- repayments.add(nextRepaymentAmount);
- step7PaybackPartialAmount(nextRepaymentAmount, today, (week+1)*7, BigDecimal.ZERO);
+ final BigDecimal interestAccruedBeforePayment = interestAccrued;
+ final BigDecimal nextRepaymentAmount = findNextRepaymentAmount(today.plusDays((week+1)*7));
+ final Payment payment = step7PaybackPartialAmount(nextRepaymentAmount, today.plusDays((week + 1) * 7), BigDecimal.ZERO);
+ payments.add(payment);
+ final BigDecimal interestAccrual = payment.getBalanceAdjustments().remove(AccountDesignators.INTEREST_ACCRUAL); //Don't compare these with planned payment.
+ final BigDecimal customerLoanInterest = payment.getBalanceAdjustments().remove(AccountDesignators.CUSTOMER_LOAN_INTEREST);
+ Assert.assertEquals("week " + week, interestAccrual.negate(), customerLoanInterest);
+ Assert.assertEquals("week " + week, interestAccruedBeforePayment, customerLoanInterest);
+ Assert.assertEquals("week " + week, plannedPayments.get(week+1).getPayment(), payment);
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);
-
-
- step8Close();
+ step8Close(DateConverter.fromIsoString(plannedPayments.get(plannedPayments.size()-1).getPayment().getDate()));
}
@Test
@@ -197,65 +260,63 @@
step1CreateProduct();
step2CreateCase();
- step3OpenCase();
- step4ApproveCase();
+ step3OpenCase(today);
+ step4ApproveCase(today);
+
+ final List<PlannedPayment> plannedPayments = individualLending.getPaymentScheduleForCaseStream(
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ null)
+ .collect(Collectors.toList());
+
step5Disburse(
BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
+ today,
UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
int week = 0;
final int weekOfLateRepayment = 3;
- final List<BigDecimal> repayments = new ArrayList<>();
- while (expectedCurrentBalance.compareTo(BigDecimal.ZERO) > 0) {
- logger.info("Simulating week {}. Expected current balance {}.", week, expectedCurrentBalance);
+ while (expectedCurrentPrincipal.compareTo(BigDecimal.ZERO) > 0) {
+ logger.info("Simulating week {}. Expected current balance {}.", week, expectedCurrentPrincipal);
if (week == weekOfLateRepayment) {
- final BigDecimal lateFee = BigDecimal.valueOf(14_49, MINOR_CURRENCY_UNIT_DIGITS);
+ final BigDecimal lateFee = BigDecimal.valueOf(15_36, MINOR_CURRENCY_UNIT_DIGITS); //??? TODO: check the late fee value.
step6CalculateInterestAndCheckForLatenessForRangeOfDays(
today,
(week * 7) + 1,
(week + 1) * 7 + 2,
8,
lateFee);
- final BigDecimal nextRepaymentAmount = findNextRepaymentAmount(today, (week + 1) * 7 + 2);
- repayments.add(nextRepaymentAmount);
- step7PaybackPartialAmount(nextRepaymentAmount, today, (week + 1) * 7 + 2, lateFee);
+ final BigDecimal nextRepaymentAmount = findNextRepaymentAmount(today.plusDays((week + 1) * 7 + 2));
+ step7PaybackPartialAmount(nextRepaymentAmount, today.plusDays((week + 1) * 7 + 2), lateFee);
}
else {
step6CalculateInterestAndCheckForLatenessForWeek(today, week);
- final BigDecimal nextRepaymentAmount = findNextRepaymentAmount(today, (week + 1) * 7);
- repayments.add(nextRepaymentAmount);
- step7PaybackPartialAmount(nextRepaymentAmount, today, (week + 1) * 7, BigDecimal.ZERO);
+ final BigDecimal nextRepaymentAmount = findNextRepaymentAmount(today.plusDays((week + 1) * 7));
+ final Payment payment = step7PaybackPartialAmount(nextRepaymentAmount, today.plusDays((week + 1) * 7), BigDecimal.ZERO);
+ final BigDecimal interestAccrual = payment.getBalanceAdjustments().remove(AccountDesignators.INTEREST_ACCRUAL); //Don't compare these with planned payment.
+ final BigDecimal customerLoanInterest = payment.getBalanceAdjustments().remove(AccountDesignators.CUSTOMER_LOAN_INTEREST);
+ Assert.assertEquals(interestAccrual.negate(), customerLoanInterest);
+ //Assert.assertEquals(plannedPayments.get(week+1).getPayment(), payment);
}
week++;
}
- 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);
-
-
- step8Close();
+ step8Close(DateConverter.fromIsoString(plannedPayments.get(plannedPayments.size()-1).getPayment().getDate()));
}
private BigDecimal findNextRepaymentAmount(
- final LocalDateTime referenceDate,
- final int dayNumber) {
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
-
- final List<CostComponent> costComponentsForNextPayment = portfolioManager.getCostComponentsForAction(
+ final LocalDateTime forDateTime) {
+ 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()
- .orElseThrow(() -> new IllegalArgumentException("return missing repayment charge."))
- .getAmount();
+ DateConverter.toIsoString(forDateTime));
+ final BigDecimal nextRepaymentAmount = nextPayment.getBalanceAdjustments()
+ .getOrDefault(AccountDesignators.ENTRY, BigDecimal.ZERO).negate();
+ Assert.assertTrue(nextRepaymentAmount.signum() != -1);
+ return nextRepaymentAmount;
}
//Create product and set charges to fixed fees.
@@ -277,7 +338,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 +358,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 +375,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));
@@ -324,15 +385,16 @@
}
//Open the case and accept a processing fee.
- private void step3OpenCase() throws InterruptedException {
+ private void step3OpenCase(final LocalDateTime forDateTime) throws InterruptedException {
logger.info("step3OpenCase");
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.OPEN,
- Collections.singleton(AccountDesignators.ENTRY),
null,
- new CostComponent(ChargeIdentifiers.PROCESSING_FEE_ID, PROCESSING_FEE_AMOUNT));
+ null,
+ forDateTime,
+ MINOR_CURRENCY_UNIT_DIGITS);
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
@@ -341,23 +403,20 @@
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
- );
}
//Deny the case. Once this is done, no more actions are possible for the case.
- private void step4DenyCase() throws InterruptedException {
+ private void step4DenyCase(final LocalDateTime forDateTime) throws InterruptedException {
logger.info("step4DenyCase");
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DENY,
- Collections.singleton(AccountDesignators.ENTRY),
- null);
+ null,
+ null,
+ forDateTime,
+ MINOR_CURRENCY_UNIT_DIGITS);
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
@@ -370,7 +429,8 @@
//Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
- private void step4ApproveCase() throws InterruptedException {
+ private void step4ApproveCase(final LocalDateTime forDateTime) throws InterruptedException
+ {
logger.info("step4ApproveCase");
markTaskExecuted(product, customerCase, taskDefinition);
@@ -379,9 +439,10 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.APPROVE,
- Collections.singleton(AccountDesignators.ENTRY),
null,
- new CostComponent(ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, LOAN_ORIGINATION_FEE_AMOUNT));
+ null,
+ forDateTime,
+ MINOR_CURRENCY_UNIT_DIGITS);
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
@@ -391,35 +452,43 @@
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 String customerLoanLedgerIdentifier = AccountingFixture.verifyLedgerCreation(
+ 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()));
+ customerLoanPrincipalIdentifier =
+ AccountingFixture.verifyAccountCreationMatchingDesignator(ledgerManager, customerLoanLedgerIdentifier, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, AccountType.ASSET);
+ customerLoanInterestIdentifier =
+ AccountingFixture.verifyAccountCreationMatchingDesignator(ledgerManager, customerLoanLedgerIdentifier, AccountDesignators.CUSTOMER_LOAN_INTEREST, AccountType.ASSET);
+ customerLoanFeeIdentifier =
+ AccountingFixture.verifyAccountCreationMatchingDesignator(ledgerManager, customerLoanLedgerIdentifier, AccountDesignators.CUSTOMER_LOAN_FEES, AccountType.ASSET);
- 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;
+ expectedCurrentPrincipal = BigDecimal.ZERO;
+ interestAccrued = BigDecimal.ZERO;
+ nonLateFees = BigDecimal.ZERO;
+ lateFees = BigDecimal.ZERO;
+ updateBalanceMock();
}
//Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
private void step5Disburse(
final BigDecimal amount,
+ final LocalDateTime forDateTime,
final String whichDisbursementFee,
final BigDecimal disbursementFeeAmount) throws InterruptedException {
- logger.info("step5Disburse");
+ logger.info("step5Disburse '{}'", amount);
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE,
- Collections.singleton(AccountDesignators.ENTRY),
- amount, new CostComponent(whichDisbursementFee, disbursementFeeAmount),
+ Sets.newLinkedHashSet(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN_GROUP),
+ amount,
+ forDateTime,
+ MINOR_CURRENCY_UNIT_DIGITS,
+ 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,19 +503,23 @@
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(customerLoanPrincipalIdentifier, amount.toPlainString()));
+ debtors.add(new Debtor(customerLoanFeeIdentifier, PROCESSING_FEE_AMOUNT.add(disbursementFeeAmount).add(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);
+ expectedCurrentPrincipal = expectedCurrentPrincipal.add(amount);
+ interestAccrued = BigDecimal.ZERO;
+ nonLateFees = nonLateFees.add(disbursementFeeAmount).add(PROCESSING_FEE_AMOUNT).add(LOAN_ORIGINATION_FEE_AMOUNT);
+ lateFees = BigDecimal.ZERO;
+
+ updateBalanceMock();
}
private void step6CalculateInterestAndCheckForLatenessForWeek(
@@ -457,25 +530,26 @@
(weekNumber * 7) + 1,
(weekNumber + 1) * 7,
-1,
- null);
+ BigDecimal.ZERO);
}
private void step6CalculateInterestAndCheckForLatenessForRangeOfDays(
final LocalDateTime referenceDate,
final int startInclusive,
final int endInclusive,
- final int dayOfLateFee,
+ final int relativeDayOfLateFee,
final BigDecimal calculatedLateFee) throws InterruptedException {
try {
+ final LocalDateTime absoluteDayOfLateFee = referenceDate.plusDays(startInclusive + relativeDayOfLateFee);
IntStream.rangeClosed(startInclusive, endInclusive)
.mapToObj(referenceDate::plusDays)
.forEach(day -> {
try {
- if (day.equals(referenceDate.plusDays(dayOfLateFee))) {
+ if (day.equals(absoluteDayOfLateFee)) {
step6CalculateInterestAccrualAndCheckForLateness(day, calculatedLateFee);
}
else {
- step6CalculateInterestAccrualAndCheckForLateness(day, null);
+ step6CalculateInterestAccrualAndCheckForLateness(day, BigDecimal.ZERO);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
@@ -493,34 +567,42 @@
//Perform daily interest calculation.
private void step6CalculateInterestAccrualAndCheckForLateness(
- final LocalDateTime forTime,
+ final LocalDateTime forDateTime,
final BigDecimal calculatedLateFee) throws InterruptedException {
- logger.info("step6CalculateInterestAccrualAndCheckForLateness");
+ logger.info("step6CalculateInterestAccrualAndCheckForLateness '{}'", forDateTime);
final String beatIdentifier = "alignment0";
- final String midnightTimeStamp = DateConverter.toIsoString(forTime);
+ final String midnightTimeStamp = DateConverter.toIsoString(forDateTime);
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
+ final BigDecimal dailyInterestRate = Fixture.INTEREST_RATE
+ .divide(BigDecimal.valueOf(100), 8, BigDecimal.ROUND_HALF_EVEN)
+ .divide(Fixture.ACCRUAL_PERIODS, 8, BigDecimal.ROUND_HALF_EVEN);
- final BigDecimal calculatedInterest = expectedCurrentBalance.multiply(Fixture.INTEREST_RATE.divide(Fixture.ACCRUAL_PERIODS, 8, BigDecimal.ROUND_HALF_EVEN))
+ final BigDecimal calculatedInterest = expectedCurrentPrincipal
+ .multiply(dailyInterestRate)
.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
+ logger.info("calculatedInterest '{}'", calculatedInterest);
+ logger.info("calculatedLateFee '{}'", calculatedLateFee);
+
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.APPLY_INTEREST,
- Collections.singleton(AccountDesignators.CUSTOMER_LOAN),
null,
- new CostComponent(ChargeIdentifiers.INTEREST_ID, calculatedInterest));
+ null,
+ forDateTime,
+ MINOR_CURRENCY_UNIT_DIGITS);
- if (calculatedLateFee != null) {
+ if (calculatedLateFee.compareTo(BigDecimal.ZERO) != 0) {
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.MARK_LATE,
- Collections.singleton(AccountDesignators.CUSTOMER_LOAN),
null,
- new CostComponent(ChargeIdentifiers.LATE_FEE_ID, calculatedLateFee));
+ null,
+ forDateTime,
+ MINOR_CURRENCY_UNIT_DIGITS);
}
final BeatPublish interestBeat = new BeatPublish(beatIdentifier, midnightTimeStamp);
portfolioBeatListener.publishBeat(interestBeat);
@@ -537,49 +619,73 @@
final Case customerCaseAfterStateChange = portfolioManager.getCase(product.getIdentifier(), customerCase.getIdentifier());
Assert.assertEquals(customerCaseAfterStateChange.getCurrentState(), Case.State.ACTIVE.name());
-
- interestAccrued = interestAccrued.add(calculatedInterest);
-
final Set<Debtor> debtors = new HashSet<>();
debtors.add(new Debtor(
- customerLoanAccountIdentifier,
+ customerLoanInterestIdentifier,
calculatedInterest.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
creditors.add(new Creditor(
AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER,
calculatedInterest.toPlainString()));
- AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.APPLY_INTEREST);
+ AccountingFixture.verifyTransfer(
+ ledgerManager,
+ debtors,
+ creditors,
+ product.getIdentifier(),
+ customerCase.getIdentifier(), Action.APPLY_INTEREST);
- expectedCurrentBalance = expectedCurrentBalance.add(calculatedInterest);
+
+ if (calculatedLateFee.compareTo(BigDecimal.ZERO) != 0) {
+ final Set<Debtor> lateFeeDebtors = new HashSet<>();
+ lateFeeDebtors.add(new Debtor(
+ customerLoanFeeIdentifier,
+ calculatedLateFee.toPlainString()));
+
+ final Set<Creditor> lateFeeCreditors = new HashSet<>();
+ lateFeeCreditors.add(new Creditor(
+ AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER,
+ calculatedLateFee.toPlainString()));
+ AccountingFixture.verifyTransfer(
+ ledgerManager,
+ lateFeeDebtors,
+ lateFeeCreditors,
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ Action.MARK_LATE);
+ lateFees = lateFees.add(calculatedLateFee);
+ }
+ interestAccrued = interestAccrued.add(calculatedInterest);
+
+ updateBalanceMock();
+ logger.info("Completed step6CalculateInterestAccrualAndCheckForLateness");
}
- private void step7PaybackPartialAmount(
+ private Payment step7PaybackPartialAmount(
final BigDecimal amount,
- final LocalDateTime referenceDate,
- final int dayNumber,
+ final LocalDateTime forDateTime,
final BigDecimal lateFee) throws InterruptedException {
- logger.info("step7PaybackPartialAmount '{}'", amount);
+ logger.info("step7PaybackPartialAmount '{}' '{}'", amount, forDateTime);
+ final BigDecimal principal = amount.subtract(interestAccrued).subtract(lateFee.add(nonLateFees));
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
-
- final BigDecimal principal = amount.subtract(interestAccrued).subtract(lateFee);
-
- checkCostComponentForActionCorrect(
+ final Payment payment = checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.ACCEPT_PAYMENT,
- new HashSet<>(Arrays.asList(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN, AccountDesignators.LOAN_FUNDS_SOURCE)),
+ new HashSet<>(Arrays.asList(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN_GROUP, AccountDesignators.LOAN_FUNDS_SOURCE)),
amount,
- new CostComponent(ChargeIdentifiers.REPAYMENT_ID, amount),
- new CostComponent(ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID, principal),
+ forDateTime,
+ MINOR_CURRENCY_UNIT_DIGITS,
+ new CostComponent(ChargeIdentifiers.REPAY_PRINCIPAL_ID, principal),
+ new CostComponent(ChargeIdentifiers.REPAY_INTEREST_ID, interestAccrued),
+ new CostComponent(ChargeIdentifiers.REPAY_FEES_ID, lateFee.add(nonLateFees)),
new CostComponent(ChargeIdentifiers.INTEREST_ID, interestAccrued),
new CostComponent(ChargeIdentifiers.LATE_FEE_ID, lateFee));
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.ACCEPT_PAYMENT,
- referenceDate.plusDays(dayNumber),
+ forDateTime,
Collections.singletonList(assignEntryToTeller()),
amount,
IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
@@ -589,38 +695,57 @@
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()));
- if (interestAccrued.compareTo(BigDecimal.ZERO) != 0)
+ BigDecimal tellerOneDebit = principal;
+ if (interestAccrued.compareTo(BigDecimal.ZERO) != 0) {
+ tellerOneDebit = tellerOneDebit.add(interestAccrued);
debtors.add(new Debtor(AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
- if (lateFee.compareTo(BigDecimal.ZERO) != 0)
+ }
+ if (lateFee.add(nonLateFees).compareTo(BigDecimal.ZERO) != 0) {
+ tellerOneDebit = tellerOneDebit.add(lateFee.add(nonLateFees));
+ }
+ if (lateFee.compareTo(BigDecimal.ZERO) != 0) {
debtors.add(new Debtor(AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER, lateFee.toPlainString()));
+ }
+ debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, tellerOneDebit.toPlainString()));
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()));
- if (interestAccrued.compareTo(BigDecimal.ZERO) != 0)
+ creditors.add(new Creditor(customerLoanPrincipalIdentifier, principal.toPlainString()));
+ if (interestAccrued.compareTo(BigDecimal.ZERO) != 0) {
+ creditors.add(new Creditor(customerLoanInterestIdentifier, interestAccrued.toPlainString()));
creditors.add(new Creditor(AccountingFixture.CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
- if (lateFee.compareTo(BigDecimal.ZERO) != 0)
+ }
+ if (lateFee.add(nonLateFees).compareTo(BigDecimal.ZERO) != 0) {
+ creditors.add(new Creditor(customerLoanFeeIdentifier, lateFee.add(nonLateFees).toPlainString()));
+ }
+ if (lateFee.compareTo(BigDecimal.ZERO) != 0) {
creditors.add(new Creditor(AccountingFixture.LATE_FEE_INCOME_ACCOUNT_IDENTIFIER, lateFee.toPlainString()));
+ }
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.ACCEPT_PAYMENT);
- expectedCurrentBalance = expectedCurrentBalance.subtract(amount).add(lateFee);
+ expectedCurrentPrincipal = expectedCurrentPrincipal.subtract(principal);
interestAccrued = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
+ nonLateFees = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
+ lateFees = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
+
+ updateBalanceMock();
+ logger.info("Completed step7PaybackPartialAmount");
+ return payment;
}
- private void step8Close() throws InterruptedException {
+ private void step8Close(
+ final LocalDateTime forDateTime) throws InterruptedException
+ {
logger.info("step8Close");
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance.negate());
-
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.CLOSE,
- Collections.singleton(AccountDesignators.ENTRY),
- null);
+ null,
+ null,
+ forDateTime,
+ MINOR_CURRENCY_UNIT_DIGITS);
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
@@ -631,4 +756,18 @@
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
}
+
+ private void updateBalanceMock() {
+ logger.info("Updating balance mocks");
+ final BigDecimal allFees = lateFees.add(nonLateFees);
+ AccountingFixture.mockBalance(customerLoanPrincipalIdentifier, expectedCurrentPrincipal);
+ AccountingFixture.mockBalance(customerLoanFeeIdentifier, allFees);
+ AccountingFixture.mockBalance(customerLoanInterestIdentifier, interestAccrued);
+ AccountingFixture.mockBalance(AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER, interestAccrued);
+ AccountingFixture.mockBalance(AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER, lateFees);
+ logger.info("updated currentPrincipal '{}'", expectedCurrentPrincipal);
+ logger.info("updated interestAccrued '{}'", interestAccrued);
+ logger.info("updated nonLateFees '{}'", nonLateFees);
+ logger.info("updated lateFees '{}'", lateFees);
+ }
}
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestCases.java b/component-test/src/main/java/io/mifos/portfolio/TestCases.java
index b6afb42..0206d75 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCases.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCases.java
@@ -21,6 +21,7 @@
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.caseinstance.CreditWorthinessFactor;
import io.mifos.individuallending.api.v1.domain.caseinstance.CreditWorthinessSnapshot;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.CasePage;
@@ -37,9 +38,6 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.CUSTOMER_LOAN;
-import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.ENTRY;
-
/**
* @author Myrle Krantz
*/
@@ -96,8 +94,8 @@
final Case caseInstance = createAdjustedCase(product.getIdentifier(), x -> x.setParameters(originalParameters));
final Set<AccountAssignment> accountAssignments = new HashSet<>();
- accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN, "002-011"));
- accountAssignments.add(new AccountAssignment(ENTRY, "002-012"));
+ accountAssignments.add(new AccountAssignment(AccountDesignators.CUSTOMER_LOAN_GROUP, "002-011"));
+ accountAssignments.add(new AccountAssignment(AccountDesignators.ENTRY, "002-012"));
caseInstance.setAccountAssignments(accountAssignments);
newCaseParameters.setMaximumBalance(Fixture.fixScale(BigDecimal.TEN));
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..59c0c31 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
@@ -18,9 +18,7 @@
import io.mifos.core.api.util.NotFoundException;
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
-import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
-import io.mifos.portfolio.api.v1.client.ChargeDefinitionIsReadOnly;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.Product;
import io.mifos.portfolio.api.v1.events.ChargeDefinitionEvent;
@@ -55,16 +53,6 @@
.map(ChargeDefinition::getIdentifier)
.collect(Collectors.toSet());
- 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());
final Set<String> expectedChangeableChargeDefinitionIdentifiers = Stream.of(
ChargeIdentifiers.DISBURSEMENT_FEE_ID,
ChargeIdentifiers.LATE_FEE_ID,
@@ -72,7 +60,7 @@
ChargeIdentifiers.PROCESSING_FEE_ID)
.collect(Collectors.toSet());
- Assert.assertEquals(expectedReadOnlyChargeDefinitionIdentifiers, readOnlyChargeDefinitionIdentifiers);
+ Assert.assertTrue(readOnlyChargeDefinitionIdentifiers.isEmpty()); //Not using readonly any more. Simply not returning charges instead.
Assert.assertEquals(expectedChangeableChargeDefinitionIdentifiers, changeableChargeDefinitionIdentifiers);
}
@@ -87,7 +75,7 @@
chargeDefinitionToDelete.setDescription("blah blah blah");
chargeDefinitionToDelete.setChargeAction(Action.APPROVE.name());
chargeDefinitionToDelete.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
- chargeDefinitionToDelete.setToAccountDesignator(AccountDesignators.ARREARS_ALLOWANCE);
+ chargeDefinitionToDelete.setToAccountDesignator(AccountDesignators.GENERAL_LOSS_ALLOWANCE);
chargeDefinitionToDelete.setFromAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
portfolioManager.createChargeDefinition(product.getIdentifier(), chargeDefinitionToDelete);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_CHARGE_DEFINITION,
@@ -105,11 +93,11 @@
catch (final NotFoundException ignored) { }
}
- @Test(expected = ChargeDefinitionIsReadOnly.class)
+ @Test(expected = NotFoundException.class)
public void shouldNotDeleteReadOnlyChargeDefinition() throws InterruptedException {
final Product product = createProduct();
- portfolioManager.deleteChargeDefinition(product.getIdentifier(), ChargeIdentifiers.ALLOW_FOR_WRITE_OFF_ID);
+ portfolioManager.deleteChargeDefinition(product.getIdentifier(), ChargeIdentifiers.INTEREST_ID);
}
@Test
@@ -175,29 +163,13 @@
}
@Test
- public void shouldNotChangeDisbursalChargeDefinition() throws InterruptedException {
+ public void shouldNotGetDisbursalChargeDefinition() throws InterruptedException {
final Product product = createProduct();
- final ChargeDefinition originalDisbursalChargeDefinition
- = portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.DISBURSE_PAYMENT_ID);
-
- final ChargeDefinition disbursalChargeDefinition
- = portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.DISBURSE_PAYMENT_ID);
- disbursalChargeDefinition.setProportionalTo(ChargeProportionalDesignator.NOT_PROPORTIONAL.getValue());
- disbursalChargeDefinition.setReadOnly(false);
-
try {
- portfolioManager.changeChargeDefinition(
- product.getIdentifier(),
- disbursalChargeDefinition.getIdentifier(),
- disbursalChargeDefinition);
- Assert.fail("Changing a readonly charge definition should fail.");
+ portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.DISBURSE_PAYMENT_ID);
+ Assert.fail("Getting a charge derived from configuration should fail.");
}
- catch (final ChargeDefinitionIsReadOnly ignore) { }
-
- final ChargeDefinition chargeDefinitionAsChanged
- = portfolioManager.getChargeDefinition(product.getIdentifier(), disbursalChargeDefinition.getIdentifier());
-
- Assert.assertEquals(originalDisbursalChargeDefinition, chargeDefinitionAsChanged);
+ catch (final NotFoundException ignore) { }
}
}
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..4294623 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_PRINCIPAL).scale());
});
}
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestLossProvisionSteps.java b/component-test/src/main/java/io/mifos/portfolio/TestLossProvisionSteps.java
new file mode 100644
index 0000000..f86ef33
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/portfolio/TestLossProvisionSteps.java
@@ -0,0 +1,48 @@
+/*
+ * 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.portfolio;
+
+import io.mifos.individuallending.api.v1.domain.product.LossProvisionConfiguration;
+import io.mifos.individuallending.api.v1.domain.product.LossProvisionStep;
+import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
+import io.mifos.portfolio.api.v1.domain.Product;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestLossProvisionSteps extends AbstractPortfolioTest {
+ @Test
+ public void shouldChangeAndGetLossProvisionSteps() throws InterruptedException {
+ final Product product = createAdjustedProduct(x -> {});
+ final List<LossProvisionStep> lossProvisionSteps = new ArrayList<>();
+ lossProvisionSteps.add(new LossProvisionStep(0, BigDecimal.valueOf(1_00, 2)));
+ lossProvisionSteps.add(new LossProvisionStep(1, BigDecimal.valueOf(9_00, 2)));
+ lossProvisionSteps.add(new LossProvisionStep(30, BigDecimal.valueOf(35_00, 2)));
+ lossProvisionSteps.add(new LossProvisionStep(60, BigDecimal.valueOf(55_00, 2)));
+ final LossProvisionConfiguration lossProvisionConfiguration = new LossProvisionConfiguration(lossProvisionSteps);
+ individualLending.changeLossProvisionConfiguration(product.getIdentifier(), lossProvisionConfiguration);
+
+ Assert.assertTrue(eventRecorder.wait(IndividualLoanEventConstants.PUT_LOSS_PROVISION_STEPS, product.getIdentifier()));
+ final LossProvisionConfiguration lossProvisionConfigurationAsSaved = individualLending.getLossProvisionConfiguration(product.getIdentifier());
+ Assert.assertEquals(lossProvisionConfiguration, lossProvisionConfigurationAsSaved);
+ }
+}
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestPatterns.java b/component-test/src/main/java/io/mifos/portfolio/TestPatterns.java
index b46f6e6..a3e83c9 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestPatterns.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestPatterns.java
@@ -15,8 +15,6 @@
*/
package io.mifos.portfolio;
-import io.mifos.core.api.util.NotFoundException;
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.Pattern;
import org.junit.Assert;
import org.junit.Test;
@@ -33,17 +31,4 @@
Assert.assertNotNull(allPatterns);
Assert.assertTrue(allPatterns.size() > 0);
}
-
- @Test
- public void shouldReturnDefaultCharges() {
- final List<ChargeDefinition> chargeDefinitions =
- portfolioManager.getAllDefaultChargeDefinitionsForPattern("io.mifos.individuallending.api.v1");
- Assert.assertNotNull(chargeDefinitions);
- Assert.assertTrue(chargeDefinitions.size() > 0);
- }
-
- @Test(expected = NotFoundException.class)
- public void shouldNotReturnDefaultChargesForNonExistentPackage() {
- portfolioManager.getAllDefaultChargeDefinitionsForPattern("io.mifos.nonexistentproduct.api.v1");
- }
}
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestSuite.java b/component-test/src/main/java/io/mifos/portfolio/TestSuite.java
index 44d8cc3..0be9417 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestSuite.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestSuite.java
@@ -32,7 +32,8 @@
TestPatterns.class,
TestProducts.class,
TestTaskDefinitions.class,
- TestTaskInstances.class
+ TestTaskInstances.class,
+ TestLossProvisionSteps.class
})
public class TestSuite extends SuiteTestEnvironment {
}
diff --git a/component-test/src/main/java/io/mifos/portfolio/listener/LossProvisionStepsEventListener.java b/component-test/src/main/java/io/mifos/portfolio/listener/LossProvisionStepsEventListener.java
new file mode 100644
index 0000000..b6bc1c4
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/portfolio/listener/LossProvisionStepsEventListener.java
@@ -0,0 +1,51 @@
+/*
+ * 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.portfolio.listener;
+
+import io.mifos.core.lang.config.TenantHeaderFilter;
+import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Component
+public class LossProvisionStepsEventListener {
+ private final EventRecorder eventRecorder;
+
+ @Autowired
+ public LossProvisionStepsEventListener(final EventRecorder eventRecorder) {
+ super();
+ this.eventRecorder = eventRecorder;
+ }
+
+ @JmsListener(
+ subscription = IndividualLoanEventConstants.DESTINATION,
+ destination = IndividualLoanEventConstants.DESTINATION,
+ selector = IndividualLoanEventConstants.SELECTOR_PUT_LOSS_PROVISION_STEPS
+ )
+ public void onChangeLossProvisionSteps(
+ @Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+ final String payload)
+ {
+ this.eventRecorder.event(tenant, IndividualLoanEventConstants.PUT_LOSS_PROVISION_STEPS, payload, String.class);
+ }
+}
\ No newline at end of file
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..59c2f47 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -15,26 +15,25 @@
*/
package io.mifos.individuallending;
-import com.google.common.collect.Sets;
import com.google.gson.Gson;
+import io.mifos.accounting.api.v1.domain.AccountType;
import io.mifos.core.lang.ServiceException;
import io.mifos.customer.api.v1.client.CustomerManager;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
-import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
import io.mifos.individuallending.internal.repository.CaseCreditWorthinessFactorEntity;
import io.mifos.individuallending.internal.repository.CaseParametersEntity;
import io.mifos.individuallending.internal.repository.CaseParametersRepository;
import io.mifos.individuallending.internal.repository.CreditWorthinessFactorType;
-import io.mifos.individuallending.internal.service.CostComponentService;
+import io.mifos.individuallending.internal.service.ChargeDefinitionService;
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.costcomponent.*;
+import io.mifos.portfolio.api.v1.domain.*;
import io.mifos.portfolio.service.ServiceConstants;
+import io.mifos.portfolio.service.internal.util.AccountingAdapter;
import io.mifos.products.spi.PatternFactory;
import io.mifos.products.spi.ProductCommandDispatcher;
import org.springframework.beans.factory.annotation.Autowired;
@@ -43,15 +42,12 @@
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
+import java.time.LocalDate;
import java.time.LocalDateTime;
-import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.*;
-import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
-
/**
* @author Myrle Krantz
*/
@@ -59,9 +55,83 @@
@Component
public class IndividualLendingPatternFactory implements PatternFactory {
final static private String INDIVIDUAL_LENDING_PACKAGE = "io.mifos.individuallending.api.v1";
+ final static private Pattern INDIVIDUAL_LENDING_PATTERN;
+
+ static {
+ INDIVIDUAL_LENDING_PATTERN = new Pattern();
+ INDIVIDUAL_LENDING_PATTERN.setParameterPackage(INDIVIDUAL_LENDING_PACKAGE);
+ INDIVIDUAL_LENDING_PATTERN.setAccountAssignmentGroups(Collections.singleton(AccountDesignators.CUSTOMER_LOAN_GROUP));
+ final Set<RequiredAccountAssignment> individualLendingRequiredAccounts = new HashSet<>();
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.CUSTOMER_LOAN_PRINCIPAL,
+ AccountType.ASSET.name(),
+ AccountDesignators.CUSTOMER_LOAN_GROUP));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.CUSTOMER_LOAN_INTEREST,
+ AccountType.ASSET.name(),
+ AccountDesignators.CUSTOMER_LOAN_GROUP));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountType.ASSET.name(),
+ AccountDesignators.CUSTOMER_LOAN_GROUP));
+
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.LOAN_FUNDS_SOURCE,
+ AccountType.ASSET.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.PROCESSING_FEE_INCOME,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.ORIGINATION_FEE_INCOME,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.DISBURSEMENT_FEE_INCOME,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.INTEREST_INCOME,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.INTEREST_ACCRUAL,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.LATE_FEE_INCOME,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.LATE_FEE_ACCRUAL,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.PRODUCT_LOSS_ALLOWANCE,
+ AccountType.ASSET.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.GENERAL_LOSS_ALLOWANCE,
+ AccountType.EXPENSE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.GENERAL_EXPENSE,
+ AccountType.EXPENSE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.ENTRY,
+ AccountType.LIABILITY.name()));
+ INDIVIDUAL_LENDING_PATTERN.setAccountAssignmentsRequired(individualLendingRequiredAccounts);
+ }
+
+
+ public static Pattern individualLendingPattern() {
+ return INDIVIDUAL_LENDING_PATTERN;
+ }
+
private final CaseParametersRepository caseParametersRepository;
private final DataContextService dataContextService;
- private final CostComponentService costComponentService;
+ private final OpenPaymentBuilderService openPaymentBuilderService;
+ private final ApprovePaymentBuilderService approvePaymentBuilderService;
+ private final DenyPaymentBuilderService denyPaymentBuilderService;
+ private final DisbursePaymentBuilderService disbursePaymentBuilderService;
+ private final ApplyInterestPaymentBuilderService applyInterestPaymentBuilderService;
+ private final AcceptPaymentBuilderService acceptPaymentBuilderService;
+ private final ClosePaymentBuilderService closePaymentBuilderService;
+ private final MarkLatePaymentBuilderService markLatePaymentBuilderService;
+ private final WriteOffPaymentBuilderService writeOffPaymentBuilderService;
+ private final RecoverPaymentBuilderService recoverPaymentBuilderService;
+ private final AccountingAdapter accountingAdapter;
private final CustomerManager customerManager;
private final IndividualLendingCommandDispatcher individualLendingCommandDispatcher;
private final Gson gson;
@@ -70,14 +140,34 @@
IndividualLendingPatternFactory(
final CaseParametersRepository caseParametersRepository,
final DataContextService dataContextService,
- final CostComponentService costComponentService,
- final CustomerManager customerManager,
+ final OpenPaymentBuilderService openPaymentBuilderService,
+ final ApprovePaymentBuilderService approvePaymentBuilderService,
+ final DenyPaymentBuilderService denyPaymentBuilderService,
+ final DisbursePaymentBuilderService disbursePaymentBuilderService,
+ final ApplyInterestPaymentBuilderService applyInterestPaymentBuilderService,
+ final AcceptPaymentBuilderService acceptPaymentBuilderService,
+ final ClosePaymentBuilderService closePaymentBuilderService,
+ final MarkLatePaymentBuilderService markLatePaymentBuilderService,
+ final WriteOffPaymentBuilderService writeOffPaymentBuilderService,
+ final RecoverPaymentBuilderService recoverPaymentBuilderService,
+ AccountingAdapter accountingAdapter, final CustomerManager customerManager,
final IndividualLendingCommandDispatcher individualLendingCommandDispatcher,
@Qualifier(ServiceConstants.GSON_NAME) final Gson gson)
{
this.caseParametersRepository = caseParametersRepository;
this.dataContextService = dataContextService;
- this.costComponentService = costComponentService;
+ this.openPaymentBuilderService = openPaymentBuilderService;
+ this.approvePaymentBuilderService = approvePaymentBuilderService;
+ this.denyPaymentBuilderService = denyPaymentBuilderService;
+ this.disbursePaymentBuilderService = disbursePaymentBuilderService;
+ this.applyInterestPaymentBuilderService = applyInterestPaymentBuilderService;
+ this.acceptPaymentBuilderService = acceptPaymentBuilderService;
+ this.closePaymentBuilderService = closePaymentBuilderService;
+ this.markLatePaymentBuilderService = markLatePaymentBuilderService;
+ this.writeOffPaymentBuilderService = writeOffPaymentBuilderService;
+ this.recoverPaymentBuilderService = recoverPaymentBuilderService;
+ this.accountingAdapter = accountingAdapter;
+
this.customerManager = customerManager;
this.individualLendingCommandDispatcher = individualLendingCommandDispatcher;
this.gson = gson;
@@ -85,169 +175,12 @@
@Override
public Pattern pattern() {
-
- final Set<String> individualLendingRequiredAccounts = new HashSet<>();
- individualLendingRequiredAccounts.add(CUSTOMER_LOAN);
- individualLendingRequiredAccounts.add(PENDING_DISBURSAL);
- individualLendingRequiredAccounts.add(LOAN_FUNDS_SOURCE);
- individualLendingRequiredAccounts.add(LOAN_FUNDS_SOURCE);
- individualLendingRequiredAccounts.add(PROCESSING_FEE_INCOME);
- individualLendingRequiredAccounts.add(ORIGINATION_FEE_INCOME);
- individualLendingRequiredAccounts.add(DISBURSEMENT_FEE_INCOME);
- individualLendingRequiredAccounts.add(INTEREST_INCOME);
- individualLendingRequiredAccounts.add(INTEREST_ACCRUAL);
- individualLendingRequiredAccounts.add(LATE_FEE_INCOME);
- individualLendingRequiredAccounts.add(LATE_FEE_ACCRUAL);
- individualLendingRequiredAccounts.add(ARREARS_ALLOWANCE);
- individualLendingRequiredAccounts.add(ENTRY);
- return new Pattern(INDIVIDUAL_LENDING_PACKAGE, individualLendingRequiredAccounts);
+ return INDIVIDUAL_LENDING_PATTERN;
}
@Override
- public List<ChargeDefinition> charges() {
- return defaultIndividualLoanCharges();
- }
-
- 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);
- processingFee.setReadOnly(false);
-
- final ChargeDefinition loanOriginationFee = charge(
- LOAN_ORIGINATION_FEE_NAME,
- Action.APPROVE,
- BigDecimal.ONE,
- ENTRY,
- ORIGINATION_FEE_INCOME);
- loanOriginationFee.setReadOnly(false);
-
- final ChargeDefinition loanFundsAllocation = charge(
- LOAN_FUNDS_ALLOCATION_ID,
- Action.APPROVE,
- BigDecimal.valueOf(100),
- LOAN_FUNDS_SOURCE,
- PENDING_DISBURSAL);
- loanFundsAllocation.setReadOnly(true);
-
- final ChargeDefinition disbursementFee = charge(
- DISBURSEMENT_FEE_NAME,
- Action.DISBURSE,
- BigDecimal.valueOf(0.1),
- ENTRY,
- DISBURSEMENT_FEE_INCOME);
- disbursementFee.setReadOnly(false);
-
- final ChargeDefinition disbursePayment = new ChargeDefinition();
- disbursePayment.setChargeAction(Action.DISBURSE.name());
- disbursePayment.setIdentifier(DISBURSE_PAYMENT_ID);
- disbursePayment.setName(DISBURSE_PAYMENT_NAME);
- disbursePayment.setDescription(DISBURSE_PAYMENT_NAME);
- disbursePayment.setFromAccountDesignator(LOANS_PAYABLE);
- disbursePayment.setToAccountDesignator(ENTRY);
- disbursePayment.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_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);
- trackPrincipalDisbursePayment.setName(TRACK_DISBURSAL_PAYMENT_NAME);
- trackPrincipalDisbursePayment.setDescription(TRACK_DISBURSAL_PAYMENT_NAME);
- trackPrincipalDisbursePayment.setFromAccountDesignator(PENDING_DISBURSAL);
- trackPrincipalDisbursePayment.setToAccountDesignator(CUSTOMER_LOAN);
- trackPrincipalDisbursePayment.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
- trackPrincipalDisbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
- trackPrincipalDisbursePayment.setAmount(BigDecimal.valueOf(100));
- trackPrincipalDisbursePayment.setReadOnly(true);
-
- final ChargeDefinition lateFee = charge(
- LATE_FEE_NAME,
- Action.ACCEPT_PAYMENT,
- BigDecimal.TEN,
- CUSTOMER_LOAN,
- LATE_FEE_INCOME);
- lateFee.setAccrueAction(Action.MARK_LATE.name());
- lateFee.setAccrualAccountDesignator(LATE_FEE_ACCRUAL);
- lateFee.setProportionalTo(ChargeProportionalDesignator.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);
- writeOffAllowanceCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
- writeOffAllowanceCharge.setReadOnly(true);
-
- final ChargeDefinition interestCharge = charge(
- INTEREST_NAME,
- Action.ACCEPT_PAYMENT,
- BigDecimal.valueOf(100),
- CUSTOMER_LOAN,
- INTEREST_INCOME);
- interestCharge.setForCycleSizeUnit(ChronoUnit.YEARS);
- interestCharge.setAccrueAction(Action.APPLY_INTEREST.name());
- interestCharge.setAccrualAccountDesignator(INTEREST_ACCRUAL);
- interestCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
- interestCharge.setChargeMethod(ChargeDefinition.ChargeMethod.INTEREST);
- interestCharge.setReadOnly(true);
-
- final ChargeDefinition customerRepaymentCharge = new ChargeDefinition();
- customerRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
- 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.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
- customerRepaymentCharge.setAmount(BigDecimal.valueOf(100));
- customerRepaymentCharge.setReadOnly(true);
-
- 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.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
- trackReturnPrincipalCharge.setAmount(BigDecimal.valueOf(100));
- trackReturnPrincipalCharge.setReadOnly(true);
-
- 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);
-
- ret.add(processingFee);
- ret.add(loanOriginationFee);
- ret.add(loanFundsAllocation);
- ret.add(disbursementFee);
- ret.add(disbursePayment);
- ret.add(trackPrincipalDisbursePayment);
- ret.add(lateFee);
- ret.add(writeOffAllowanceCharge);
- ret.add(interestCharge);
- ret.add(customerRepaymentCharge);
- ret.add(trackReturnPrincipalCharge);
- ret.add(disbursementReturnCharge);
-
- return ret;
+ public Stream<ChargeDefinition> defaultConfigurableCharges() {
+ return ChargeDefinitionService.defaultConfigurableIndividualLoanCharges();
}
@Override
@@ -344,7 +277,7 @@
}
@Override
- public List<CostComponent> getCostComponentsForAction(
+ public Payment getCostComponentsForAction(
final String productIdentifier,
final String caseIdentifier,
final String actionIdentifier,
@@ -356,35 +289,67 @@
final Case.State caseState = Case.State.valueOf(dataContextOfAction.getCustomerCaseEntity().getCurrentState());
checkActionCanBeExecuted(caseState, action);
- Stream<Map.Entry<ChargeDefinition, CostComponent>> costComponentStream = costComponentService.getCostComponentsForAction(
+ return getPaymentForAction(
action,
dataContextOfAction,
+ forAccountDesignators,
forPaymentSize,
- forDateTime.toLocalDate())
- .stream();
-
- if (!forAccountDesignators.isEmpty()) {
- costComponentStream = costComponentStream
- .filter(costComponentEntry -> chargeReferencesAccountDesignators(costComponentEntry.getKey(), action, forAccountDesignators));
- }
-
- return costComponentStream
- .map(costComponentEntry -> new CostComponent(costComponentEntry.getKey().getIdentifier(), costComponentEntry.getValue().getAmount()))
- .collect(Collectors.toList());
+ forDateTime.toLocalDate());
}
- private boolean chargeReferencesAccountDesignators(
- final ChargeDefinition chargeDefinition,
+ private Payment getPaymentForAction(
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());
+ final DataContextOfAction dataContextOfAction,
+ final Set<String> forAccountDesignators,
+ final BigDecimal forPaymentSize,
+ final LocalDate forDate) {
+ final PaymentBuilderService paymentBuilderService;
+ switch (action) {
+ case OPEN:
+ paymentBuilderService = openPaymentBuilderService;
+ break;
+ case APPROVE:
+ paymentBuilderService = approvePaymentBuilderService;
+ break;
+ case DENY:
+ paymentBuilderService = denyPaymentBuilderService;
+ break;
+ case DISBURSE:
+ paymentBuilderService = disbursePaymentBuilderService;
+ break;
+ case APPLY_INTEREST:
+ paymentBuilderService = applyInterestPaymentBuilderService;
+ break;
+ case ACCEPT_PAYMENT:
+ paymentBuilderService = acceptPaymentBuilderService;
+ break;
+ case CLOSE:
+ paymentBuilderService = closePaymentBuilderService;
+ break;
+ case MARK_LATE:
+ paymentBuilderService = markLatePaymentBuilderService;
+ break;
+ case WRITE_OFF:
+ paymentBuilderService = writeOffPaymentBuilderService;
+ break;
+ case RECOVER:
+ paymentBuilderService = recoverPaymentBuilderService;
+ break;
+ default:
+ throw ServiceException.internalError("Invalid action: ''{0}''.", action.name());
+ }
- return !Sets.intersection(accountsToCompare, forAccountDesignators).isEmpty();
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ dataContextOfAction);
+
+ final PaymentBuilder paymentBuilder = paymentBuilderService.getPaymentBuilder(
+ dataContextOfAction,
+ forPaymentSize,
+ forDate,
+ runningBalances);
+
+ return paymentBuilder.buildPayment(action, forAccountDesignators, forDate);
}
public static void checkActionCanBeExecuted(final Case.State state, final Action action) {
@@ -414,26 +379,4 @@
public ProductCommandDispatcher getIndividualLendingCommandDispatcher() {
return this.individualLendingCommandDispatcher;
}
-
- private static ChargeDefinition charge(
- final String name,
- final Action action,
- final BigDecimal defaultAmount,
- final String fromAccount,
- final String toAccount)
- {
- final ChargeDefinition ret = new ChargeDefinition();
-
- ret.setIdentifier(name.toLowerCase(Locale.US).replace(" ", "-"));
- ret.setName(name);
- ret.setDescription(name);
- ret.setChargeAction(action.name());
- ret.setAmount(defaultAmount);
- ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
- ret.setProportionalTo(ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue());
- ret.setFromAccountDesignator(fromAccount);
- ret.setToAccountDesignator(toAccount);
-
- return ret;
- }
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/ChangeLossProvisionSteps.java b/service/src/main/java/io/mifos/individuallending/internal/command/ChangeLossProvisionSteps.java
new file mode 100644
index 0000000..7468b78
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/ChangeLossProvisionSteps.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.command;
+
+import io.mifos.individuallending.api.v1.domain.product.LossProvisionConfiguration;
+
+/**
+ * @author Myrle Krantz
+ */
+public class ChangeLossProvisionSteps {
+ private final String productIdentifier;
+ private final LossProvisionConfiguration lossProvisionConfiguration;
+
+ public ChangeLossProvisionSteps(String productIdentifier, LossProvisionConfiguration lossProvisionConfiguration) {
+ this.productIdentifier = productIdentifier;
+ this.lossProvisionConfiguration = lossProvisionConfiguration;
+ }
+
+ public String getProductIdentifier() {
+ return productIdentifier;
+ }
+
+ public LossProvisionConfiguration getLossProvisionConfiguration() {
+ return lossProvisionConfiguration;
+ }
+
+ @Override
+ public String toString() {
+ return "ChangeLossProvisionSteps{" +
+ "productIdentifier='" + productIdentifier + '\'' +
+ ", lossProvisionConfiguration=" + lossProvisionConfiguration +
+ '}';
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
index cb63a4f..b476d47 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
@@ -31,9 +31,14 @@
import io.mifos.individuallending.internal.command.CheckLateCommand;
import io.mifos.individuallending.internal.command.MarkLateCommand;
import io.mifos.individuallending.internal.service.*;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.Period;
+import io.mifos.individuallending.internal.service.schedule.ScheduledActionHelpers;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.service.config.PortfolioProperties;
import io.mifos.portfolio.service.internal.command.CreateBeatPublishCommand;
+import io.mifos.portfolio.service.internal.repository.CaseCommandEntity;
+import io.mifos.portfolio.service.internal.repository.CaseCommandRepository;
import io.mifos.portfolio.service.internal.repository.CaseEntity;
import io.mifos.portfolio.service.internal.repository.CaseRepository;
import io.mifos.portfolio.service.internal.util.AccountingAdapter;
@@ -41,6 +46,10 @@
import io.mifos.rhythm.spi.v1.events.BeatPublishEvent;
import io.mifos.rhythm.spi.v1.events.EventConstants;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@@ -58,6 +67,7 @@
@Aggregate
public class BeatPublishCommandHandler {
private final CaseRepository caseRepository;
+ private final CaseCommandRepository caseCommandRepository;
private final PortfolioProperties portfolioProperties;
private final DataContextService dataContextService;
private final ApplicationName applicationName;
@@ -67,12 +77,14 @@
@Autowired
public BeatPublishCommandHandler(
final CaseRepository caseRepository,
+ final CaseCommandRepository caseCommandRepository,
final PortfolioProperties portfolioProperties,
final DataContextService dataContextService,
final ApplicationName applicationName,
final CommandBus commandBus,
final AccountingAdapter accountingAdapter) {
this.caseRepository = caseRepository;
+ this.caseCommandRepository = caseCommandRepository;
this.portfolioProperties = portfolioProperties;
this.dataContextService = dataContextService;
this.applicationName = applicationName;
@@ -127,16 +139,17 @@
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
+ final String customerLoanPrincipalAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ final String customerLoanInterestAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_INTEREST);
final String lateFeeAccrualAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.LATE_FEE_ACCRUAL);
- final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+ final BigDecimal currentBalance = accountingAdapter.getCurrentAccountBalance(customerLoanPrincipalAccountIdentifier);
if (currentBalance.compareTo(BigDecimal.ZERO) == 0) //No late fees if the current balance is zilch.
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getForTime());
final LocalDateTime dateOfMostRecentDisbursement =
- accountingAdapter.getDateOfMostRecentEntryContainingMessage(customerLoanAccountIdentifier, dataContextOfAction.getMessageForCharge(Action.DISBURSE))
+ accountingAdapter.getDateOfMostRecentEntryContainingMessage(customerLoanPrincipalAccountIdentifier, dataContextOfAction.getMessageForCharge(Action.DISBURSE))
.orElseThrow(() ->
ServiceException.badRequest("No last disbursal date for ''{0}.{1}'' could be determined. " +
"Therefore it cannot be checked for lateness.", productIdentifier, caseIdentifier));
@@ -154,18 +167,23 @@
.getPaymentSize()
.multiply(BigDecimal.valueOf(repaymentPeriodsBetweenBeginningAndToday));
- final BigDecimal paymentsSum = accountingAdapter.sumMatchingEntriesSinceDate(
- customerLoanAccountIdentifier,
+ final BigDecimal principalSum = accountingAdapter.sumMatchingEntriesSinceDate(
+ customerLoanPrincipalAccountIdentifier,
dateOfMostRecentDisbursement.toLocalDate(),
dataContextOfAction.getMessageForCharge(Action.ACCEPT_PAYMENT));
+ final BigDecimal interestSum = accountingAdapter.sumMatchingEntriesSinceDate(
+ customerLoanInterestAccountIdentifier,
+ dateOfMostRecentDisbursement.toLocalDate(),
+ dataContextOfAction.getMessageForCharge(Action.ACCEPT_PAYMENT));
+ final BigDecimal paymentsSum = principalSum.add(interestSum);
final BigDecimal lateFeesAccrued = accountingAdapter.sumMatchingEntriesSinceDate(
lateFeeAccrualAccountIdentifier,
dateOfMostRecentDisbursement.toLocalDate(),
dataContextOfAction.getMessageForCharge(Action.MARK_LATE));
- if (paymentsSum.compareTo(expectedPaymentSum.add(lateFeesAccrued)) < 0) {
- final Optional<LocalDateTime> dateOfMostRecentLateFee = accountingAdapter.getDateOfMostRecentEntryContainingMessage(customerLoanAccountIdentifier, dataContextOfAction.getMessageForCharge(Action.MARK_LATE));
+ if (paymentsSum.compareTo(expectedPaymentSum) < 0) {
+ final Optional<LocalDateTime> dateOfMostRecentLateFee = dateOfMostRecentMarkLate(dataContextOfAction.getCustomerCaseEntity().getId());
if (!dateOfMostRecentLateFee.isPresent() ||
mostRecentLateFeeIsBeforeMostRecentRepaymentPeriod(repaymentPeriods, dateOfMostRecentLateFee.get())) {
commandBus.dispatch(new MarkLateCommand(productIdentifier, caseIdentifier, command.getForTime()));
@@ -175,6 +193,16 @@
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getForTime());
}
+ private Optional<LocalDateTime> dateOfMostRecentMarkLate(final Long caseId) {
+ final Pageable pageRequest = new PageRequest(0, 10, Sort.Direction.DESC, "createdOn");
+ final Page<CaseCommandEntity> page = caseCommandRepository.findByCaseIdAndActionName(
+ caseId,
+ Action.MARK_LATE.name(),
+ pageRequest);
+
+ return page.getContent().stream().findFirst().map(CaseCommandEntity::getCreatedOn);
+ }
+
private boolean mostRecentLateFeeIsBeforeMostRecentRepaymentPeriod(
final List<Period> repaymentPeriods,
final LocalDateTime dateOfMostRecentLateFee) {
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..23f313f 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
@@ -16,6 +16,7 @@
package io.mifos.individuallending.internal.command.handler;
+import io.mifos.core.api.util.UserContextHolder;
import io.mifos.core.command.annotation.Aggregate;
import io.mifos.core.command.annotation.CommandHandler;
import io.mifos.core.command.annotation.CommandLogLevel;
@@ -29,18 +30,17 @@
import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
import io.mifos.individuallending.internal.command.*;
import io.mifos.individuallending.internal.repository.CaseParametersRepository;
-import io.mifos.individuallending.internal.service.*;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.DataContextService;
+import io.mifos.individuallending.internal.service.DesignatorToAccountIdentifierMapper;
+import io.mifos.individuallending.internal.service.costcomponent.*;
+import io.mifos.individuallending.internal.service.schedule.ScheduledActionHelpers;
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;
-import io.mifos.portfolio.service.internal.repository.CaseEntity;
-import io.mifos.portfolio.service.internal.repository.CaseRepository;
-import io.mifos.portfolio.service.internal.repository.TaskInstanceRepository;
+import io.mifos.portfolio.service.internal.repository.*;
import io.mifos.portfolio.service.internal.util.AccountingAdapter;
-import io.mifos.portfolio.service.internal.util.ChargeInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
@@ -64,8 +64,18 @@
public class IndividualLoanCommandHandler {
private final CaseRepository caseRepository;
private final DataContextService dataContextService;
- private final CostComponentService costComponentService;
+ private final OpenPaymentBuilderService openPaymentBuilderService;
+ private final ApprovePaymentBuilderService approvePaymentBuilderService;
+ private final DenyPaymentBuilderService denyPaymentBuilderService;
+ private final DisbursePaymentBuilderService disbursePaymentBuilderService;
+ private final ApplyInterestPaymentBuilderService applyInterestPaymentBuilderService;
+ private final AcceptPaymentBuilderService acceptPaymentBuilderService;
+ private final ClosePaymentBuilderService closePaymentBuilderService;
+ private final MarkLatePaymentBuilderService markLatePaymentBuilderService;
+ private final WriteOffPaymentBuilderService writeOffPaymentBuilderService;
+ private final RecoverPaymentBuilderService recoverPaymentBuilderService;
private final AccountingAdapter accountingAdapter;
+ private final CaseCommandRepository caseCommandRepository;
private final TaskInstanceRepository taskInstanceRepository;
private final CaseParametersRepository caseParametersRepository;
@@ -73,14 +83,33 @@
public IndividualLoanCommandHandler(
final CaseRepository caseRepository,
final DataContextService dataContextService,
- final CostComponentService costComponentService,
+ final OpenPaymentBuilderService openPaymentBuilderService,
+ final ApprovePaymentBuilderService approvePaymentBuilderService,
+ final DenyPaymentBuilderService denyPaymentBuilderService,
+ final DisbursePaymentBuilderService disbursePaymentBuilderService,
+ final ApplyInterestPaymentBuilderService applyInterestPaymentBuilderService,
+ final AcceptPaymentBuilderService acceptPaymentBuilderService,
+ final ClosePaymentBuilderService closePaymentBuilderService,
+ final MarkLatePaymentBuilderService markLatePaymentBuilderService,
+ final WriteOffPaymentBuilderService writeOffPaymentBuilderService,
+ final RecoverPaymentBuilderService recoverPaymentBuilderService,
final AccountingAdapter accountingAdapter,
- final TaskInstanceRepository taskInstanceRepository,
+ CaseCommandRepository caseCommandRepository, final TaskInstanceRepository taskInstanceRepository,
final CaseParametersRepository caseParametersRepository) {
this.caseRepository = caseRepository;
this.dataContextService = dataContextService;
- this.costComponentService = costComponentService;
+ this.openPaymentBuilderService = openPaymentBuilderService;
+ this.approvePaymentBuilderService = approvePaymentBuilderService;
+ this.denyPaymentBuilderService = denyPaymentBuilderService;
+ this.disbursePaymentBuilderService = disbursePaymentBuilderService;
+ this.applyInterestPaymentBuilderService = applyInterestPaymentBuilderService;
+ this.acceptPaymentBuilderService = acceptPaymentBuilderService;
+ this.closePaymentBuilderService = closePaymentBuilderService;
+ this.markLatePaymentBuilderService = markLatePaymentBuilderService;
+ this.writeOffPaymentBuilderService = writeOffPaymentBuilderService;
+ this.recoverPaymentBuilderService = recoverPaymentBuilderService;
this.accountingAdapter = accountingAdapter;
+ this.caseCommandRepository = caseCommandRepository;
this.taskInstanceRepository = taskInstanceRepository;
this.caseParametersRepository = caseParametersRepository;
}
@@ -88,7 +117,7 @@
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(
- selectorName = EventConstants.SELECTOR_NAME,
+ selectorName = IndividualLoanEventConstants.SELECTOR_NAME,
selectorValue = IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final OpenCommand command) {
final String productIdentifier = command.getProductIdentifier();
@@ -99,30 +128,33 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.OPEN);
- final CostComponentsForRepaymentPeriod costComponents
- = costComponentService.getCostComponentsForOpen(dataContextOfAction);
-
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
- = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ dataContextOfAction);
- final List<ChargeInstance> charges = costComponents.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.OPEN,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final PaymentBuilder paymentBuilder
+ = openPaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, CostComponentService.today(), runningBalances);
final LocalDateTime today = today();
- accountingAdapter.bookCharges(charges,
+ final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
+ designatorToAccountIdentifierMapper,
command.getCommand().getNote(),
command.getCommand().getCreatedOn(),
dataContextOfAction.getMessageForCharge(Action.OPEN),
Action.OPEN.getTransactionType());
- //Only move to new state if book charges command was accepted.
+
final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
+
+ recordCommand(
+ command.getCommand().getCreatedOn(),
+ customerCase.getId(),
+ Action.OPEN,
+ transactionUniqueifier);
+
+ //Only move to new state if book charges command was accepted.
customerCase.setCurrentState(Case.State.PENDING.name());
caseRepository.save(customerCase);
@@ -132,7 +164,7 @@
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(
- selectorName = EventConstants.SELECTOR_NAME,
+ selectorName = IndividualLoanEventConstants.SELECTOR_NAME,
selectorValue = IndividualLoanEventConstants.DENY_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final DenyCommand command) {
final String productIdentifier = command.getProductIdentifier();
@@ -143,36 +175,58 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.DENY);
- final CostComponentsForRepaymentPeriod costComponents
- = costComponentService.getCostComponentsForDeny(dataContextOfAction);
-
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ dataContextOfAction);
- final List<ChargeInstance> charges = costComponents.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.DENY,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final PaymentBuilder paymentBuilder
+ = denyPaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, CostComponentService.today(), runningBalances);
final LocalDateTime today = today();
+ final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
+ designatorToAccountIdentifierMapper,
+ command.getCommand().getNote(),
+ command.getCommand().getCreatedOn(),
+ dataContextOfAction.getMessageForCharge(Action.DENY),
+ Action.DENY.getTransactionType());
+
final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
+
+ recordCommand(
+ command.getCommand().getCreatedOn(),
+ customerCase.getId(),
+ Action.DENY,
+ transactionUniqueifier);
+
customerCase.setCurrentState(Case.State.CLOSED.name());
caseRepository.save(customerCase);
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
}
+ static class InterruptedInALambdaException extends RuntimeException {
+
+ private final InterruptedException interruptedException;
+
+ InterruptedInALambdaException(InterruptedException e) {
+ interruptedException = e;
+ }
+
+ void throwWrappedException() throws InterruptedException {
+ throw interruptedException;
+ }
+ }
+
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(
- selectorName = EventConstants.SELECTOR_NAME,
+ selectorName = IndividualLoanEventConstants.SELECTOR_NAME,
selectorValue = IndividualLoanEventConstants.APPROVE_INDIVIDUALLOAN_CASE)
- public IndividualLoanCommandEvent process(final ApproveCommand command) {
+ public IndividualLoanCommandEvent process(final ApproveCommand command) throws InterruptedException
+ {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
@@ -184,39 +238,65 @@
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ //Create the needed account assignments for groups and persist them for the case.
+ try {
+ designatorToAccountIdentifierMapper.getGroupsNeedingLedgers()
+ .map(groupNeedingLedger -> {
+ try {
+ final String createdLedgerIdentifier = accountingAdapter.createLedger(
+ dataContextOfAction.getCaseParametersEntity().getCustomerIdentifier(),
+ groupNeedingLedger.getGroupName(),
+ groupNeedingLedger.getParentLedger());
+ return new AccountAssignment(groupNeedingLedger.getGroupName(), createdLedgerIdentifier);
+ } catch (InterruptedException e) {
+ throw new InterruptedInALambdaException(e);
+ }
+ })
+ .map(accountAssignment -> CaseMapper.map(accountAssignment, dataContextOfAction.getCustomerCaseEntity()))
+ .forEach(caseAccountAssignmentEntity -> dataContextOfAction.getCustomerCaseEntity().getAccountAssignments().add(caseAccountAssignmentEntity));
+ }
+ catch (final InterruptedInALambdaException e) {
+ e.throwWrappedException();
+ }
+
//Create the needed account assignments and persist them for the case.
designatorToAccountIdentifierMapper.getLedgersNeedingAccounts()
- .map(ledger ->
- new AccountAssignment(ledger.getDesignator(),
- accountingAdapter.createAccountForLedgerAssignment(dataContextOfAction.getCaseParametersEntity().getCustomerIdentifier(), ledger)))
- .map(accountAssignment -> CaseMapper.map(accountAssignment, dataContextOfAction.getCustomerCaseEntity()))
- .forEach(caseAccountAssignmentEntity ->
- dataContextOfAction.getCustomerCaseEntity().getAccountAssignments().add(caseAccountAssignmentEntity)
- );
+ .map(ledger ->
+ new AccountAssignment(ledger.getDesignator(),
+ accountingAdapter.createAccountForLedgerAssignment(
+ dataContextOfAction.getCaseParametersEntity().getCustomerIdentifier(),
+ ledger)))
+ .map(accountAssignment -> CaseMapper.map(accountAssignment, dataContextOfAction.getCustomerCaseEntity()))
+ .forEach(caseAccountAssignmentEntity ->
+ dataContextOfAction.getCustomerCaseEntity().getAccountAssignments().add(caseAccountAssignmentEntity)
+ );
caseRepository.save(dataContextOfAction.getCustomerCaseEntity());
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- costComponentService.getCostComponentsForApprove(dataContextOfAction);
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ dataContextOfAction);
- final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.APPROVE,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final PaymentBuilder paymentBuilder =
+ approvePaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, CostComponentService.today(), runningBalances);
final LocalDateTime today = today();
- accountingAdapter.bookCharges(charges,
+ final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
+ designatorToAccountIdentifierMapper,
command.getCommand().getNote(),
command.getCommand().getCreatedOn(),
dataContextOfAction.getMessageForCharge(Action.APPROVE),
Action.APPROVE.getTransactionType());
- //Only move to new state if book charges command was accepted.
final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
+
+ recordCommand(
+ command.getCommand().getCreatedOn(),
+ customerCase.getId(),
+ Action.APPROVE,
+ transactionUniqueifier);
+
+ //Only move to new state if book charges command was accepted.
customerCase.setCurrentState(Case.State.APPROVED.name());
caseRepository.save(customerCase);
@@ -225,7 +305,7 @@
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
- @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.DISBURSE_INDIVIDUALLOAN_CASE)
+ @EventEmitter(selectorName = IndividualLoanEventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.DISBURSE_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final DisburseCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
@@ -236,32 +316,35 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.DISBURSE);
final BigDecimal disbursalAmount = Optional.ofNullable(command.getCommand().getPaymentSize()).orElse(BigDecimal.ZERO);
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- costComponentService.getCostComponentsForDisburse(dataContextOfAction, disbursalAmount);
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ dataContextOfAction);
- final List<ChargeInstance> charges =
- costComponentsForRepaymentPeriod.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.DISBURSE,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final PaymentBuilder paymentBuilder =
+ disbursePaymentBuilderService.getPaymentBuilder(dataContextOfAction, disbursalAmount, CostComponentService.today(), runningBalances);
final LocalDateTime today = today();
- accountingAdapter.bookCharges(charges,
+ final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
+ designatorToAccountIdentifierMapper,
command.getCommand().getNote(),
command.getCommand().getCreatedOn(),
dataContextOfAction.getMessageForCharge(Action.DISBURSE),
Action.DISBURSE.getTransactionType());
+
+ final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
+
+ recordCommand(
+ command.getCommand().getCreatedOn(),
+ customerCase.getId(),
+ Action.DISBURSE,
+ transactionUniqueifier);
+
//Only move to new state if book charges command was accepted.
if (Case.State.valueOf(dataContextOfAction.getCustomerCaseEntity().getCurrentState()) != Case.State.ACTIVE) {
- final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
final LocalDateTime endOfTerm
= ScheduledActionHelpers.getRoughEndDate(today.toLocalDate(), dataContextOfAction.getCaseParameters())
.atTime(LocalTime.MIDNIGHT);
@@ -269,11 +352,10 @@
customerCase.setCurrentState(Case.State.ACTIVE.name());
caseRepository.save(customerCase);
}
- final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
- final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
+ final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP);
- final BigDecimal newLoanPaymentSize = costComponentService.getLoanPaymentSize(
- currentBalance.add(disbursalAmount),
+ final BigDecimal newLoanPaymentSize = disbursePaymentBuilderService.getLoanPaymentSizeForSingleDisbursement(
+ currentBalance.add(paymentBuilder.getBalanceAdjustment(AccountDesignators.ENTRY)),
dataContextOfAction);
dataContextOfAction.getCaseParametersEntity().setPaymentSize(newLoanPaymentSize);
@@ -285,7 +367,7 @@
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(
- selectorName = EventConstants.SELECTOR_NAME,
+ selectorName = IndividualLoanEventConstants.SELECTOR_NAME,
selectorValue = IndividualLoanEventConstants.APPLY_INTEREST_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final ApplyInterestCommand command) {
final String productIdentifier = command.getProductIdentifier();
@@ -298,34 +380,37 @@
throw ServiceException.internalError(
"End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- costComponentService.getCostComponentsForApplyInterest(dataContextOfAction);
-
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ 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 PaymentBuilder paymentBuilder =
+ applyInterestPaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, CostComponentService.today(), runningBalances);
- accountingAdapter.bookCharges(charges,
+ final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
+ designatorToAccountIdentifierMapper,
"Applied interest on " + command.getForTime(),
command.getForTime(),
dataContextOfAction.getMessageForCharge(Action.APPLY_INTEREST),
Action.APPLY_INTEREST.getTransactionType());
+ final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
+
+ recordCommand(
+ command.getForTime(),
+ customerCase.getId(),
+ Action.APPLY_INTEREST,
+ transactionUniqueifier);
+
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getForTime());
}
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(
- selectorName = EventConstants.SELECTOR_NAME,
+ selectorName = IndividualLoanEventConstants.SELECTOR_NAME,
selectorValue = IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final AcceptPaymentCommand command) {
final String productIdentifier = command.getProductIdentifier();
@@ -340,39 +425,43 @@
throw ServiceException.internalError(
"End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- costComponentService.getCostComponentsForAcceptPayment(
- dataContextOfAction,
- command.getCommand().getPaymentSize(),
- DateConverter.fromIsoString(command.getCommand().getCreatedOn()).toLocalDate());
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ 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 PaymentBuilder paymentBuilder =
+ acceptPaymentBuilderService.getPaymentBuilder(
+ dataContextOfAction,
+ command.getCommand().getPaymentSize(),
+ DateConverter.fromIsoString(command.getCommand().getCreatedOn()).toLocalDate(), runningBalances);
final LocalDateTime today = today();
- accountingAdapter.bookCharges(charges,
+ final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
+ designatorToAccountIdentifierMapper,
command.getCommand().getNote(),
command.getCommand().getCreatedOn(),
dataContextOfAction.getMessageForCharge(Action.ACCEPT_PAYMENT),
Action.ACCEPT_PAYMENT.getTransactionType());
+ final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
+
+ recordCommand(
+ command.getCommand().getCreatedOn(),
+ customerCase.getId(),
+ Action.ACCEPT_PAYMENT,
+ transactionUniqueifier);
+
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
}
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(
- selectorName = EventConstants.SELECTOR_NAME,
+ selectorName = IndividualLoanEventConstants.SELECTOR_NAME,
selectorValue = IndividualLoanEventConstants.MARK_LATE_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final MarkLateCommand command) {
final String productIdentifier = command.getProductIdentifier();
@@ -387,35 +476,39 @@
throw ServiceException.internalError(
"End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- costComponentService.getCostComponentsForMarkLate(dataContextOfAction, DateConverter.fromIsoString(command.getForTime()));
-
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ 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 PaymentBuilder paymentBuilder =
+ markLatePaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, DateConverter.fromIsoString(command.getForTime()).toLocalDate(),
+ runningBalances);
final LocalDateTime today = today();
- accountingAdapter.bookCharges(charges,
+ final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
+ designatorToAccountIdentifierMapper,
"Marked late on " + command.getForTime(),
command.getForTime(),
dataContextOfAction.getMessageForCharge(Action.MARK_LATE),
Action.MARK_LATE.getTransactionType());
+ final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
+
+ recordCommand(
+ command.getForTime(),
+ customerCase.getId(),
+ Action.MARK_LATE,
+ transactionUniqueifier);
+
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
}
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
- @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.WRITE_OFF_INDIVIDUALLOAN_CASE)
+ @EventEmitter(selectorName = IndividualLoanEventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.WRITE_OFF_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final WriteOffCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
@@ -424,10 +517,35 @@
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCaseEntity().getCurrentState()), Action.WRITE_OFF);
checkIfTasksAreOutstanding(dataContextOfAction, Action.WRITE_OFF);
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ dataContextOfAction);
+
+ final PaymentBuilder paymentBuilder =
+ writeOffPaymentBuilderService.getPaymentBuilder(
+ dataContextOfAction,
+ command.getCommand().getPaymentSize(),
+ DateConverter.fromIsoString(command.getCommand().getCreatedOn()).toLocalDate(), runningBalances);
final LocalDateTime today = today();
+ final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
+ designatorToAccountIdentifierMapper,
+ command.getCommand().getNote(),
+ command.getCommand().getCreatedOn(),
+ dataContextOfAction.getMessageForCharge(Action.WRITE_OFF),
+ Action.WRITE_OFF.getTransactionType());
+
final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
+
+ recordCommand(
+ command.getCommand().getCreatedOn(),
+ customerCase.getId(),
+ Action.WRITE_OFF,
+ transactionUniqueifier);
+
customerCase.setCurrentState(Case.State.CLOSED.name());
caseRepository.save(customerCase);
@@ -436,7 +554,7 @@
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
- @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.CLOSE_INDIVIDUALLOAN_CASE)
+ @EventEmitter(selectorName = IndividualLoanEventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.CLOSE_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final CloseCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
@@ -446,29 +564,23 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.CLOSE);
- final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- costComponentService.getCostComponentsForClose(dataContextOfAction);
-
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ dataContextOfAction);
- final List<ChargeInstance> charges =
- costComponentsForRepaymentPeriod.stream()
- .map(entry -> mapCostComponentEntryToChargeInstance(
- Action.DISBURSE,
- entry,
- designatorToAccountIdentifierMapper))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toList());
+ final PaymentBuilder paymentBuilder =
+ closePaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, CostComponentService.today(), runningBalances);
final LocalDateTime today = today();
- accountingAdapter.bookCharges(charges,
+ final Optional<String> transactionIdentifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
+ designatorToAccountIdentifierMapper,
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());
@@ -479,7 +591,7 @@
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
- @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.RECOVER_INDIVIDUALLOAN_CASE)
+ @EventEmitter(selectorName = IndividualLoanEventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.RECOVER_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final RecoverCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
@@ -489,45 +601,38 @@
checkIfTasksAreOutstanding(dataContextOfAction, Action.RECOVER);
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ dataContextOfAction);
+
+ final PaymentBuilder paymentBuilder =
+ recoverPaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, CostComponentService.today(), runningBalances);
+
final LocalDateTime today = today();
+ final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
+ designatorToAccountIdentifierMapper,
+ command.getCommand().getNote(),
+ command.getCommand().getCreatedOn(),
+ dataContextOfAction.getMessageForCharge(Action.RECOVER),
+ Action.CLOSE.getTransactionType());
+
final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
+
+ recordCommand(
+ command.getCommand().getCreatedOn(),
+ customerCase.getId(),
+ Action.RECOVER,
+ transactionUniqueifier);
+
customerCase.setCurrentState(Case.State.CLOSED.name());
caseRepository.save(customerCase);
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();
@@ -550,6 +655,20 @@
action.name(), productIdentifier, caseIdentifier);
}
+ private void recordCommand(
+ final String when,
+ final Long caseId,
+ final Action action,
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<String> transactionUniqueifier) {
+ final CaseCommandEntity caseCommandEntity = new CaseCommandEntity();
+ caseCommandEntity.setCaseId(caseId);
+ caseCommandEntity.setActionName(action.name());
+ caseCommandEntity.setCreatedBy(UserContextHolder.checkedGetUser());
+ caseCommandEntity.setCreatedOn(DateConverter.fromIsoString(when));
+ caseCommandEntity.setTransactionUniqueifier(transactionUniqueifier.orElse(""));
+ caseCommandRepository.save(caseCommandEntity);
+ }
+
private static LocalDateTime today() {
return LocalDate.now(Clock.systemUTC()).atStartOfDay();
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/LossProvisionStepsCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/LossProvisionStepsCommandHandler.java
new file mode 100644
index 0000000..31cd9d8
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/LossProvisionStepsCommandHandler.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.command.handler;
+
+import io.mifos.core.command.annotation.Aggregate;
+import io.mifos.core.command.annotation.CommandHandler;
+import io.mifos.core.command.annotation.CommandLogLevel;
+import io.mifos.core.command.annotation.EventEmitter;
+import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
+import io.mifos.individuallending.internal.command.ChangeLossProvisionSteps;
+import io.mifos.individuallending.internal.mapper.LossProvisionStepMapper;
+import io.mifos.individuallending.internal.repository.LossProvisionStepEntity;
+import io.mifos.individuallending.internal.repository.LossProvisionStepRepository;
+import io.mifos.portfolio.service.internal.repository.ProductEntity;
+import io.mifos.portfolio.service.internal.repository.ProductRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.stream.Stream;
+
+
+/**
+ * @author Myrle Krantz
+ */
+@Aggregate
+public class LossProvisionStepsCommandHandler {
+ private final LossProvisionStepRepository lossProvisionStepRepository;
+ private final ProductRepository productRepository;
+
+ @Autowired
+ public LossProvisionStepsCommandHandler(
+ final LossProvisionStepRepository lossProvisionStepRepository,
+ final ProductRepository productRepository) {
+ this.lossProvisionStepRepository = lossProvisionStepRepository;
+ this.productRepository = productRepository;
+ }
+
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ @EventEmitter(
+ selectorName = IndividualLoanEventConstants.SELECTOR_NAME,
+ selectorValue = IndividualLoanEventConstants.PUT_LOSS_PROVISION_STEPS)
+ public String process(final ChangeLossProvisionSteps command) {
+ final ProductEntity productEntity = productRepository.findByIdentifier(command.getProductIdentifier())
+ .orElseThrow(() -> ServiceException.notFound("Product not found ''{0}''.", command.getProductIdentifier()));
+ final Stream<LossProvisionStepEntity> lossProvisionSteps = lossProvisionStepRepository.findByProductId(productEntity.getId());
+ lossProvisionSteps.forEach(lossProvisionStepRepository::delete);
+ command.getLossProvisionConfiguration().getLossProvisionSteps().stream()
+ .map(lossProvisionStep -> LossProvisionStepMapper.map(productEntity.getId(), lossProvisionStep))
+ .forEach(lossProvisionStepRepository::save);
+
+ return command.getProductIdentifier();
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/mapper/LossProvisionStepMapper.java b/service/src/main/java/io/mifos/individuallending/internal/mapper/LossProvisionStepMapper.java
new file mode 100644
index 0000000..1b4bbbe
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/mapper/LossProvisionStepMapper.java
@@ -0,0 +1,41 @@
+/*
+ * 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.mapper;
+
+import io.mifos.individuallending.api.v1.domain.product.LossProvisionStep;
+import io.mifos.individuallending.internal.repository.LossProvisionStepEntity;
+
+import java.math.BigDecimal;
+
+public interface LossProvisionStepMapper {
+ static LossProvisionStepEntity map(
+ final Long productId,
+ final LossProvisionStep instance) {
+ final LossProvisionStepEntity ret = new LossProvisionStepEntity();
+ ret.setProductId(productId);
+ ret.setDaysLate(instance.getDaysLate());
+ ret.setPercentProvision(instance.getPercentProvision().setScale(2, BigDecimal.ROUND_HALF_EVEN));
+ return ret;
+ }
+
+ static LossProvisionStep map(
+ final LossProvisionStepEntity entity) {
+ final LossProvisionStep ret = new LossProvisionStep();
+ ret.setDaysLate(entity.getDaysLate());
+ ret.setPercentProvision(entity.getPercentProvision());
+ return ret;
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/repository/LossProvisionStepEntity.java b/service/src/main/java/io/mifos/individuallending/internal/repository/LossProvisionStepEntity.java
new file mode 100644
index 0000000..a74a8ea
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/repository/LossProvisionStepEntity.java
@@ -0,0 +1,88 @@
+/*
+ * 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.repository;
+
+import javax.persistence.*;
+import java.math.BigDecimal;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Entity
+@Table(name = "bastet_p_arrears_config")
+public class LossProvisionStepEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @Column(name = "product_id", nullable = false)
+ private Long productId;
+
+ @Column(name = "days_late", nullable = false)
+ private Integer daysLate;
+
+ @Column(name = "percent_provision", nullable = false)
+ private BigDecimal percentProvision;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getProductId() {
+ return productId;
+ }
+
+ public void setProductId(Long productId) {
+ this.productId = productId;
+ }
+
+ public Integer getDaysLate() {
+ return daysLate;
+ }
+
+ public void setDaysLate(Integer daysLate) {
+ this.daysLate = daysLate;
+ }
+
+ public BigDecimal getPercentProvision() {
+ return percentProvision;
+ }
+
+ public void setPercentProvision(BigDecimal percentProvision) {
+ this.percentProvision = percentProvision;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LossProvisionStepEntity that = (LossProvisionStepEntity) o;
+ return Objects.equals(productId, that.productId) &&
+ Objects.equals(daysLate, that.daysLate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(productId, daysLate);
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/repository/LossProvisionStepRepository.java b/service/src/main/java/io/mifos/individuallending/internal/repository/LossProvisionStepRepository.java
new file mode 100644
index 0000000..810412e
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/repository/LossProvisionStepRepository.java
@@ -0,0 +1,32 @@
+/*
+ * 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.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+@Repository
+public interface LossProvisionStepRepository extends JpaRepository<LossProvisionStepEntity, Long> {
+ Stream<LossProvisionStepEntity> findByProductId(Long id);
+
+ Optional<LossProvisionStepEntity> findByProductIdAndDaysLate(Long id, int daysLate);
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/AnnuityPayment.java b/service/src/main/java/io/mifos/individuallending/internal/service/AnnuityPayment.java
index 0350089..a872fbd 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/AnnuityPayment.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/AnnuityPayment.java
@@ -28,7 +28,7 @@
/**
* @author Myrle Krantz
*/
-final class AnnuityPayment implements MonetaryOperator {
+public final class AnnuityPayment implements MonetaryOperator {
private Rate rate;
private int periods;
@@ -52,13 +52,16 @@
return new AnnuityPayment(rate, periods);
}
- static MonetaryAmount calculate(
- final @Nonnull MonetaryAmount amount,
- final @Nonnull Rate rate,
- final @Nonnegative int periods)
+ public static MonetaryAmount calculate(
+ final @Nonnull MonetaryAmount amount,
+ final @Nonnull Rate rate,
+ final @Nonnegative int periods)
{
Objects.requireNonNull(amount, "Amount required");
Objects.requireNonNull(rate, "Rate required");
+ if (rate.get().compareTo(BigDecimal.ZERO) == 0)
+ return amount.divide(periods);
+
// AP(m) = m*r / [ (1-((1 + r).pow(-n))) ]
return amount.multiply(rate.get()).divide(
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ChargeDefinitionService.java b/service/src/main/java/io/mifos/individuallending/internal/service/ChargeDefinitionService.java
new file mode 100644
index 0000000..78d8ef5
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ChargeDefinitionService.java
@@ -0,0 +1,222 @@
+/*
+ * 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.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.service.internal.service.ConfigurableChargeDefinitionService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Nonnull;
+import java.math.BigDecimal;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class ChargeDefinitionService {
+ public static Stream<ChargeDefinition> defaultConfigurableIndividualLoanCharges() {
+ final List<ChargeDefinition> ret = new ArrayList<>();
+ final ChargeDefinition processingFee = charge(
+ PROCESSING_FEE_NAME,
+ Action.DISBURSE,
+ BigDecimal.ONE,
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountDesignators.PROCESSING_FEE_INCOME);
+ processingFee.setReadOnly(false);
+
+ final ChargeDefinition loanOriginationFee = charge(
+ LOAN_ORIGINATION_FEE_NAME,
+ Action.DISBURSE,
+ BigDecimal.ONE,
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountDesignators.ORIGINATION_FEE_INCOME);
+ loanOriginationFee.setReadOnly(false);
+
+ final ChargeDefinition disbursementFee = charge(
+ DISBURSEMENT_FEE_NAME,
+ Action.DISBURSE,
+ BigDecimal.valueOf(0.1),
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountDesignators.DISBURSEMENT_FEE_INCOME);
+ disbursementFee.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
+ disbursementFee.setReadOnly(false);
+
+ final ChargeDefinition lateFee = charge(
+ LATE_FEE_NAME,
+ Action.ACCEPT_PAYMENT,
+ BigDecimal.TEN,
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountDesignators.LATE_FEE_INCOME);
+ lateFee.setAccrueAction(Action.MARK_LATE.name());
+ lateFee.setAccrualAccountDesignator(AccountDesignators.LATE_FEE_ACCRUAL);
+ lateFee.setProportionalTo(ChargeProportionalDesignator.CONTRACTUAL_REPAYMENT_DESIGNATOR.getValue());
+ lateFee.setChargeOnTop(true);
+ lateFee.setReadOnly(false);
+
+ ret.add(processingFee);
+ ret.add(loanOriginationFee);
+ ret.add(disbursementFee);
+ ret.add(lateFee);
+
+ return ret.stream();
+ }
+
+ static Stream<ChargeDefinition> individualLoanChargesDerivedFromConfiguration() {
+ final List<ChargeDefinition> ret = new ArrayList<>();
+
+ final ChargeDefinition disbursePayment = new ChargeDefinition();
+ disbursePayment.setChargeAction(Action.DISBURSE.name());
+ disbursePayment.setIdentifier(DISBURSE_PAYMENT_ID);
+ disbursePayment.setName(DISBURSE_PAYMENT_NAME);
+ disbursePayment.setDescription(DISBURSE_PAYMENT_NAME);
+ disbursePayment.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ disbursePayment.setToAccountDesignator(AccountDesignators.ENTRY);
+ disbursePayment.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
+ disbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ disbursePayment.setAmount(BigDecimal.valueOf(100));
+ disbursePayment.setReadOnly(true);
+
+ final ChargeDefinition interestCharge = new ChargeDefinition();
+ interestCharge.setIdentifier(INTEREST_ID);
+ interestCharge.setName(INTEREST_NAME);
+ interestCharge.setDescription(INTEREST_NAME);
+ interestCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
+ interestCharge.setAmount(BigDecimal.valueOf(100));
+ interestCharge.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN_INTEREST);
+ interestCharge.setToAccountDesignator(AccountDesignators.INTEREST_INCOME);
+ interestCharge.setForCycleSizeUnit(ChronoUnit.YEARS);
+ interestCharge.setAccrueAction(Action.APPLY_INTEREST.name());
+ interestCharge.setAccrualAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
+ interestCharge.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_DESIGNATOR.getValue());
+ interestCharge.setChargeMethod(ChargeDefinition.ChargeMethod.INTEREST);
+ interestCharge.setReadOnly(true);
+
+ final ChargeDefinition customerFeeRepaymentCharge = new ChargeDefinition();
+ customerFeeRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
+ customerFeeRepaymentCharge.setIdentifier(REPAY_FEES_ID);
+ customerFeeRepaymentCharge.setName(REPAY_FEES_NAME);
+ customerFeeRepaymentCharge.setDescription(REPAY_FEES_NAME);
+ customerFeeRepaymentCharge.setFromAccountDesignator(AccountDesignators.ENTRY);
+ customerFeeRepaymentCharge.setToAccountDesignator(AccountDesignators.CUSTOMER_LOAN_FEES);
+ customerFeeRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.TO_ACCOUNT_DESIGNATOR.getValue());
+ customerFeeRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ customerFeeRepaymentCharge.setAmount(BigDecimal.valueOf(100));
+ customerFeeRepaymentCharge.setReadOnly(true);
+
+ final ChargeDefinition customerInterestRepaymentCharge = new ChargeDefinition();
+ customerInterestRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
+ customerInterestRepaymentCharge.setIdentifier(REPAY_INTEREST_ID);
+ customerInterestRepaymentCharge.setName(REPAY_INTEREST_NAME);
+ customerInterestRepaymentCharge.setDescription(REPAY_INTEREST_NAME);
+ customerInterestRepaymentCharge.setFromAccountDesignator(AccountDesignators.ENTRY);
+ customerInterestRepaymentCharge.setToAccountDesignator(AccountDesignators.CUSTOMER_LOAN_INTEREST);
+ customerInterestRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.TO_ACCOUNT_DESIGNATOR.getValue());
+ customerInterestRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ customerInterestRepaymentCharge.setAmount(BigDecimal.valueOf(100));
+ customerInterestRepaymentCharge.setReadOnly(true);
+
+ final ChargeDefinition customerPrincipalRepaymentCharge = new ChargeDefinition();
+ customerPrincipalRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
+ customerPrincipalRepaymentCharge.setIdentifier(REPAY_PRINCIPAL_ID);
+ customerPrincipalRepaymentCharge.setName(REPAY_PRINCIPAL_NAME);
+ customerPrincipalRepaymentCharge.setDescription(REPAY_PRINCIPAL_NAME);
+ customerPrincipalRepaymentCharge.setFromAccountDesignator(AccountDesignators.ENTRY);
+ customerPrincipalRepaymentCharge.setToAccountDesignator(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ customerPrincipalRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REQUESTED_REPAYMENT_DESIGNATOR.getValue());
+ customerPrincipalRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ customerPrincipalRepaymentCharge.setAmount(BigDecimal.valueOf(100));
+ customerPrincipalRepaymentCharge.setReadOnly(true);
+
+ ret.add(disbursePayment);
+ ret.add(interestCharge);
+ ret.add(customerPrincipalRepaymentCharge);
+ ret.add(customerInterestRepaymentCharge);
+ ret.add(customerFeeRepaymentCharge);
+
+ return ret.stream();
+ }
+ private final ConfigurableChargeDefinitionService configurableChargeDefinitionService;
+
+ @Autowired
+ public ChargeDefinitionService(
+ final ConfigurableChargeDefinitionService configurableChargeDefinitionService) {
+ this.configurableChargeDefinitionService = configurableChargeDefinitionService;
+ }
+
+ private Stream<ChargeDefinition> getAllChargeDefinitions(final String productIdentifier) {
+ final Stream<ChargeDefinition> configurableChargeDefinitions = configurableChargeDefinitionService.findAllEntities(productIdentifier);
+ final Stream<ChargeDefinition> derivedChargeDefinitions = individualLoanChargesDerivedFromConfiguration();
+ return Stream.concat(configurableChargeDefinitions, derivedChargeDefinitions);
+ }
+
+ @Nonnull
+ public Map<String, List<ChargeDefinition>> getChargeDefinitionsMappedByChargeAction(
+ final String productIdentifier)
+ {
+ final Stream<ChargeDefinition> chargeDefinitions = getAllChargeDefinitions(productIdentifier);
+
+ return chargeDefinitions
+ .collect(Collectors.groupingBy(ChargeDefinition::getChargeAction,
+ Collectors.mapping(x -> x, Collectors.toList())));
+ }
+
+ @Nonnull
+ public Map<String, List<ChargeDefinition>> getChargeDefinitionsMappedByAccrueAction(
+ final String productIdentifier)
+ {
+ final Stream<ChargeDefinition> chargeDefinitions = getAllChargeDefinitions(productIdentifier);
+
+ return chargeDefinitions
+ .filter(x -> x.getAccrueAction() != null)
+ .collect(Collectors.groupingBy(ChargeDefinition::getAccrueAction,
+ Collectors.mapping(x -> x, Collectors.toList())));
+ }
+
+ private static ChargeDefinition charge(
+ final String name,
+ final Action action,
+ final BigDecimal defaultAmount,
+ final String fromAccount,
+ final String toAccount)
+ {
+ final ChargeDefinition ret = new ChargeDefinition();
+
+ ret.setIdentifier(name.toLowerCase(Locale.US).replace(" ", "-"));
+ ret.setName(name);
+ ret.setDescription(name);
+ ret.setChargeAction(action.name());
+ ret.setAmount(defaultAmount);
+ ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ ret.setProportionalTo(ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue());
+ ret.setFromAccountDesignator(fromAccount);
+ ret.setToAccountDesignator(toAccount);
+
+ return ret;
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
deleted file mode 100644
index 120378b..0000000
--- a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
+++ /dev/null
@@ -1,665 +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.core.lang.ServiceException;
-import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
-import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
-import io.mifos.individuallending.api.v1.domain.workflow.Action;
-import io.mifos.individuallending.internal.repository.CaseParametersEntity;
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
-import io.mifos.portfolio.api.v1.domain.CostComponent;
-import io.mifos.portfolio.service.internal.util.AccountingAdapter;
-import org.javamoney.calc.common.Rate;
-import org.javamoney.moneta.Money;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import javax.money.MonetaryAmount;
-import java.math.BigDecimal;
-import java.time.Clock;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.util.*;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-/**
- * @author Myrle Krantz
- */
-@Service
-public class CostComponentService {
- private static final int EXTRA_PRECISION = 4;
- private static final int RUNNING_CALCULATION_PRECISION = 8;
-
- private final ScheduledChargesService scheduledChargesService;
- private final AccountingAdapter accountingAdapter;
-
- @Autowired
- public CostComponentService(
- final ScheduledChargesService scheduledChargesService,
- final AccountingAdapter accountingAdapter) {
- this.scheduledChargesService = scheduledChargesService;
- this.accountingAdapter = accountingAdapter;
- }
-
- public CostComponentsForRepaymentPeriod getCostComponentsForAction(
- final Action action,
- final DataContextOfAction dataContextOfAction,
- final BigDecimal forPaymentSize,
- final LocalDate forDate) {
- switch (action) {
- case OPEN:
- return getCostComponentsForOpen(dataContextOfAction);
- case APPROVE:
- return getCostComponentsForApprove(dataContextOfAction);
- case DENY:
- return getCostComponentsForDeny(dataContextOfAction);
- case DISBURSE:
- return getCostComponentsForDisburse(dataContextOfAction, forPaymentSize);
- case APPLY_INTEREST:
- return getCostComponentsForApplyInterest(dataContextOfAction);
- case ACCEPT_PAYMENT:
- return getCostComponentsForAcceptPayment(dataContextOfAction, forPaymentSize, forDate);
- case CLOSE:
- return getCostComponentsForClose(dataContextOfAction);
- case MARK_LATE:
- return getCostComponentsForMarkLate(dataContextOfAction, today().atStartOfDay());
- case WRITE_OFF:
- return getCostComponentsForWriteOff(dataContextOfAction);
- case RECOVER:
- return getCostComponentsForRecover(dataContextOfAction);
- default:
- throw ServiceException.internalError("Invalid action: ''{0}''.", action.name());
- }
- }
-
- public CostComponentsForRepaymentPeriod getCostComponentsForOpen(final DataContextOfAction dataContextOfAction) {
- final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
- final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
- final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
- final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.OPEN, today()));
- final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
- productIdentifier, scheduledActions);
-
- return getCostComponentsForScheduledCharges(
- Collections.emptyMap(),
- scheduledCharges,
- caseParameters.getBalanceRangeMaximum(),
- BigDecimal.ZERO,
- BigDecimal.ZERO,
- dataContextOfAction.getInterest(),
- minorCurrencyUnitDigits,
- true);
- }
-
- public CostComponentsForRepaymentPeriod getCostComponentsForDeny(final DataContextOfAction dataContextOfAction) {
- final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
- final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
- final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
- final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.DENY, today()));
- final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
- productIdentifier, scheduledActions);
-
- return getCostComponentsForScheduledCharges(
- Collections.emptyMap(),
- scheduledCharges,
- caseParameters.getBalanceRangeMaximum(),
- BigDecimal.ZERO,
- BigDecimal.ZERO,
- dataContextOfAction.getInterest(),
- minorCurrencyUnitDigits,
- true);
- }
-
- public CostComponentsForRepaymentPeriod getCostComponentsForApprove(final DataContextOfAction dataContextOfAction) {
- //Charge the approval fee if applicable.
- final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
- final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
- final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
- final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.APPROVE, today()));
- final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
- productIdentifier, scheduledActions);
-
- return getCostComponentsForScheduledCharges(
- Collections.emptyMap(),
- scheduledCharges,
- caseParameters.getBalanceRangeMaximum(),
- BigDecimal.ZERO,
- BigDecimal.ZERO,
- dataContextOfAction.getInterest(),
- minorCurrencyUnitDigits,
- true);
- }
-
- public CostComponentsForRepaymentPeriod 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();
-
- if (requestedDisbursalSize != null &&
- dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum().compareTo(
- currentBalance.add(requestedDisbursalSize)) < 0)
- throw ServiceException.conflict("Cannot disburse over the maximum balance.");
-
- final Optional<LocalDateTime> optionalStartOfTerm = accountingAdapter.getDateOfOldestEntryContainingMessage(
- customerLoanAccountIdentifier,
- dataContextOfAction.getMessageForCharge(Action.DISBURSE));
- final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
- final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
- final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
- final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.DISBURSE, today()));
-
- final BigDecimal disbursalSize;
- if (requestedDisbursalSize == null)
- disbursalSize = dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum().negate();
- else
- disbursalSize = requestedDisbursalSize.negate();
-
- final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
- productIdentifier, scheduledActions);
-
-
- final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledCharges.stream()
- .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x.getChargeDefinition(), Action.DISBURSE)));
-
- final Map<ChargeDefinition, CostComponent> accruedCostComponents =
- optionalStartOfTerm.map(startOfTerm ->
- chargesSplitIntoScheduledAndAccrued.get(true)
- .stream()
- .map(ScheduledCharge::getChargeDefinition)
- .collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
- chargeDefinition -> getAccruedCostComponentToApply(
- dataContextOfAction,
- designatorToAccountIdentifierMapper,
- startOfTerm.toLocalDate(),
- chargeDefinition)))).orElse(Collections.emptyMap());
-
- return getCostComponentsForScheduledCharges(
- accruedCostComponents,
- chargesSplitIntoScheduledAndAccrued.get(false),
- caseParameters.getBalanceRangeMaximum(),
- currentBalance,
- disbursalSize,
- dataContextOfAction.getInterest(),
- minorCurrencyUnitDigits,
- true);
- }
-
- public CostComponentsForRepaymentPeriod 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 LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
-
- final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
- final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
- final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
- final LocalDate today = today();
- final ScheduledAction interestAction = new ScheduledAction(Action.APPLY_INTEREST, today, new Period(1, today));
-
- final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
- productIdentifier,
- Collections.singletonList(interestAction));
-
- final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledCharges.stream()
- .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x.getChargeDefinition(), Action.APPLY_INTEREST)));
-
- final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
- .stream()
- .map(ScheduledCharge::getChargeDefinition)
- .collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
- chargeDefinition -> getAccruedCostComponentToApply(dataContextOfAction, designatorToAccountIdentifierMapper, startOfTerm, chargeDefinition)));
-
- return getCostComponentsForScheduledCharges(
- accruedCostComponents,
- chargesSplitIntoScheduledAndAccrued.get(false),
- caseParameters.getBalanceRangeMaximum(),
- currentBalance,
- BigDecimal.ZERO,
- dataContextOfAction.getInterest(),
- minorCurrencyUnitDigits,
- true);
- }
-
- public CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(
- final DataContextOfAction dataContextOfAction,
- final @Nullable BigDecimal requestedLoanPaymentSize,
- final LocalDate forDate)
- {
- final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
- = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
- final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
-
- final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
-
- final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
- final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
- final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
- final ScheduledAction scheduledAction
- = ScheduledActionHelpers.getNextScheduledPayment(
- startOfTerm,
- forDate,
- dataContextOfAction.getCustomerCaseEntity().getEndOfTerm().toLocalDate(),
- dataContextOfAction.getCaseParameters()
- );
-
- final List<ScheduledCharge> scheduledChargesForThisAction = scheduledChargesService.getScheduledCharges(
- productIdentifier,
- Collections.singletonList(scheduledAction));
-
- final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledChargesForThisAction.stream()
- .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x.getChargeDefinition(), Action.ACCEPT_PAYMENT)));
-
- final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
- .stream()
- .map(ScheduledCharge::getChargeDefinition)
- .collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
- chargeDefinition -> getAccruedCostComponentToApply(dataContextOfAction, designatorToAccountIdentifierMapper, startOfTerm, chargeDefinition)));
-
-
- final BigDecimal loanPaymentSize;
-
- if (requestedLoanPaymentSize != null) {
- loanPaymentSize = requestedLoanPaymentSize;
- }
- else {
- if (scheduledAction.actionPeriod != null && scheduledAction.actionPeriod.isLastPeriod()) {
- loanPaymentSize = currentBalance;
- }
- else {
- final BigDecimal paymentSizeBeforeOnTopCharges = currentBalance.min(dataContextOfAction.getCaseParametersEntity().getPaymentSize());
-
- @SuppressWarnings("UnnecessaryLocalVariable")
- final BigDecimal paymentSizeIncludingOnTopCharges = accruedCostComponents.entrySet().stream()
- .filter(entry -> entry.getKey().getChargeOnTop() != null && entry.getKey().getChargeOnTop())
- .map(entry -> entry.getValue().getAmount())
- .reduce(paymentSizeBeforeOnTopCharges, BigDecimal::add);
-
- loanPaymentSize = paymentSizeIncludingOnTopCharges;
- }
- }
-
-
- return getCostComponentsForScheduledCharges(
- accruedCostComponents,
- chargesSplitIntoScheduledAndAccrued.get(false),
- caseParameters.getBalanceRangeMaximum(),
- currentBalance,
- loanPaymentSize,
- dataContextOfAction.getInterest(),
- minorCurrencyUnitDigits,
- true);
- }
-
- public CostComponentsForRepaymentPeriod 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)
- throw ServiceException.conflict("Cannot close loan until the balance is zero.");
-
- final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
-
- final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
- final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
- final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
- final LocalDate today = today();
- final ScheduledAction closeAction = new ScheduledAction(Action.CLOSE, today, new Period(1, today));
-
- final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
- productIdentifier,
- Collections.singletonList(closeAction));
-
- final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledCharges.stream()
- .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x.getChargeDefinition(), Action.CLOSE)));
-
- final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
- .stream()
- .map(ScheduledCharge::getChargeDefinition)
- .collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
- chargeDefinition -> getAccruedCostComponentToApply(dataContextOfAction, designatorToAccountIdentifierMapper, startOfTerm, chargeDefinition)));
-
- return getCostComponentsForScheduledCharges(
- accruedCostComponents,
- chargesSplitIntoScheduledAndAccrued.get(false),
- caseParameters.getBalanceRangeMaximum(),
- currentBalance,
- BigDecimal.ZERO,
- dataContextOfAction.getInterest(),
- minorCurrencyUnitDigits,
- true);
- }
-
- public CostComponentsForRepaymentPeriod getCostComponentsForMarkLate(final DataContextOfAction dataContextOfAction,
- final LocalDateTime forTime) {
- final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
- = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
- final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
-
- final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
-
- final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
- final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
- final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
- final ScheduledAction scheduledAction = new ScheduledAction(Action.MARK_LATE, forTime.toLocalDate());
-
- final BigDecimal loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize();
-
- final List<ScheduledCharge> scheduledChargesForThisAction = scheduledChargesService.getScheduledCharges(
- productIdentifier,
- Collections.singletonList(scheduledAction));
-
- final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledChargesForThisAction.stream()
- .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x.getChargeDefinition(), Action.MARK_LATE)));
-
- final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
- .stream()
- .map(ScheduledCharge::getChargeDefinition)
- .collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
- chargeDefinition -> getAccruedCostComponentToApply(dataContextOfAction, designatorToAccountIdentifierMapper, startOfTerm, chargeDefinition)));
-
-
- return getCostComponentsForScheduledCharges(
- accruedCostComponents,
- chargesSplitIntoScheduledAndAccrued.get(false),
- caseParameters.getBalanceRangeMaximum(),
- currentBalance,
- loanPaymentSize,
- dataContextOfAction.getInterest(),
- minorCurrencyUnitDigits,
- true);
- }
-
- private CostComponentsForRepaymentPeriod getCostComponentsForWriteOff(final DataContextOfAction dataContextOfAction) {
- return null;
- }
-
- private CostComponentsForRepaymentPeriod getCostComponentsForRecover(final DataContextOfAction dataContextOfAction) {
- return null;
- }
-
- static CostComponentsForRepaymentPeriod getCostComponentsForScheduledCharges(
- final Map<ChargeDefinition, CostComponent> accruedCostComponents,
- final Collection<ScheduledCharge> scheduledCharges,
- final BigDecimal maximumBalance,
- final BigDecimal runningBalance,
- final BigDecimal entryAccountAdjustment, //disbursement or payment size.
- 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<>();
-
- 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);
- }
-
-
- for (final ScheduledCharge scheduledCharge : scheduledCharges) {
- if (accrualAccounting || !isAccrualChargeForAction(scheduledCharge.getChargeDefinition(), scheduledCharge.getScheduledAction().action)) {
- final BigDecimal amountProportionalTo = getAmountProportionalTo(
- scheduledCharge,
- maximumBalance,
- runningBalance,
- entryAccountAdjustment,
- balanceAdjustments);
- //TODO: getAmountProportionalTo is programmed under the assumption of non-accrual accounting.
- if (scheduledCharge.getChargeRange().map(x ->
- !x.amountIsWithinRange(amountProportionalTo)).orElse(false))
- continue;
-
- final CostComponent costComponent = costComponentMap
- .computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
-
- final BigDecimal chargeAmount = howToApplyScheduledChargeToAmount(scheduledCharge, interest)
- .apply(amountProportionalTo)
- .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
- 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));
- }
- }
-
- return new CostComponentsForRepaymentPeriod(
- costComponentMap,
- balanceAdjustments.getOrDefault(AccountDesignators.LOANS_PAYABLE, BigDecimal.ZERO).negate());
- }
-
- private static BigDecimal getAmountProportionalTo(
- final ScheduledCharge scheduledCharge,
- final BigDecimal maximumBalance,
- final BigDecimal runningBalance,
- final BigDecimal loanPaymentSize,
- final Map<String, BigDecimal> balanceAdjustments) {
- final Optional<ChargeProportionalDesignator> optionalChargeProportionalTo
- = ChargeProportionalDesignator.fromString(scheduledCharge.getChargeDefinition().getProportionalTo());
- return optionalChargeProportionalTo.map(chargeProportionalTo ->
- getAmountProportionalTo(chargeProportionalTo, maximumBalance, runningBalance, loanPaymentSize, balanceAdjustments))
- .orElse(BigDecimal.ZERO);
- }
-
- static BigDecimal getAmountProportionalTo(
- final ChargeProportionalDesignator chargeProportionalTo,
- final BigDecimal maximumBalance,
- final BigDecimal runningBalance,
- final BigDecimal loanPaymentSize,
- final Map<String, BigDecimal> balanceAdjustments) {
- switch (chargeProportionalTo) {
- case NOT_PROPORTIONAL:
- return BigDecimal.ZERO;
- case MAXIMUM_BALANCE_DESIGNATOR:
- return maximumBalance;
- case RUNNING_BALANCE_DESIGNATOR:
- return runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
- case REPAYMENT_DESIGNATOR:
- return loanPaymentSize;
- case PRINCIPAL_ADJUSTMENT_DESIGNATOR: {
- 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();
- }
- 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)
- {
- switch (scheduledCharge.getChargeDefinition().getChargeMethod())
- {
- case FIXED: {
- return (amountProportionalTo) -> scheduledCharge.getChargeDefinition().getAmount();
- }
- case PROPORTIONAL: {
- final BigDecimal chargeAmountPerPeriod = PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, scheduledCharge.getChargeDefinition().getAmount(), RUNNING_CALCULATION_PRECISION);
- return chargeAmountPerPeriod::multiply;
- }
- case INTEREST: {
- final BigDecimal chargeAmountPerPeriod = PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, interest, RUNNING_CALCULATION_PRECISION);
- return chargeAmountPerPeriod::multiply;
- }
- default: {
- return (amountProportionalTo) -> BigDecimal.ZERO;
- }
- }
- }
-
- public BigDecimal getLoanPaymentSize(
- final BigDecimal assumedBalance,
- final DataContextOfAction dataContextOfAction) {
- final List<ScheduledAction> hypotheticalScheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(
- today(),
- dataContextOfAction.getCaseParameters());
- final List<ScheduledCharge> hypotheticalScheduledCharges = scheduledChargesService.getScheduledCharges(
- dataContextOfAction.getProductEntity().getIdentifier(),
- hypotheticalScheduledActions);
- return getLoanPaymentSize(
- assumedBalance,
- 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;
- final Map<Period, BigDecimal> accrualRatesByPeriod
- = PeriodChargeCalculator.getPeriodAccrualInterestRate(interest, scheduledCharges, precision);
-
- final int periodCount = accrualRatesByPeriod.size();
- if (periodCount == 0)
- return startingBalance;
-
- final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream()
- .collect(RateCollectors.geometricMean(precision));
-
- final MonetaryAmount presentValue = AnnuityPayment.calculate(
- Money.of(startingBalance, "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());
- }
-
- private static boolean isAccrualChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
- return chargeDefinition.getAccrueAction() != null &&
- chargeDefinition.getAccrueAction().equals(action.name());
- }
-
- private CostComponent getAccruedCostComponentToApply(final DataContextOfAction dataContextOfAction,
- final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper,
- final LocalDate startOfTerm,
- final ChargeDefinition chargeDefinition) {
- final CostComponent ret = new CostComponent();
-
- final String accrualAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator());
-
- final BigDecimal amountAccrued = accountingAdapter.sumMatchingEntriesSinceDate(
- accrualAccountIdentifier,
- startOfTerm,
- dataContextOfAction.getMessageForCharge(Action.valueOf(chargeDefinition.getAccrueAction())));
- final BigDecimal amountApplied = accountingAdapter.sumMatchingEntriesSinceDate(
- accrualAccountIdentifier,
- startOfTerm,
- dataContextOfAction.getMessageForCharge(Action.valueOf(chargeDefinition.getChargeAction())));
-
- ret.setChargeIdentifier(chargeDefinition.getIdentifier());
- ret.setAmount(amountAccrued.subtract(amountApplied));
- return ret;
- }
-
- private LocalDate getStartOfTermOrThrow(final DataContextOfAction dataContextOfAction,
- final String customerLoanAccountIdentifier) {
- final Optional<LocalDateTime> firstDisbursalDateTime = accountingAdapter.getDateOfOldestEntryContainingMessage(
- customerLoanAccountIdentifier,
- dataContextOfAction.getMessageForCharge(Action.DISBURSE));
-
- return firstDisbursalDateTime.map(LocalDateTime::toLocalDate)
- .orElseThrow(() -> ServiceException.internalError(
- "Start of term for loan ''{0}'' could not be acquired from accounting.",
- dataContextOfAction.getCompoundIdentifer()));
- }
-
- private static LocalDate today() {
- return LocalDate.now(Clock.systemUTC());
- }
-
-}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
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/DataContextOfAction.java b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
index 3d0e968..90c0328 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
@@ -38,10 +38,12 @@
private final CaseParametersEntity caseParameters;
private final List<AccountAssignment> oneTimeAccountAssignments;
- DataContextOfAction(final @Nonnull ProductEntity product,
- final @Nonnull CaseEntity customerCase,
- final @Nonnull CaseParametersEntity caseParameters,
- final @Nullable List<AccountAssignment> oneTimeAccountAssignments) {
+ public DataContextOfAction(
+ final @Nonnull ProductEntity product,
+ final @Nonnull CaseEntity customerCase,
+ final @Nonnull CaseParametersEntity caseParameters,
+ final @Nullable List<AccountAssignment> oneTimeAccountAssignments)
+ {
this.product = product;
this.customerCase = customerCase;
this.caseParameters = caseParameters;
@@ -68,7 +70,7 @@
return oneTimeAccountAssignments;
}
- String getCompoundIdentifer() {
+ public String getCompoundIdentifer() {
return product.getIdentifier() + "." + customerCase.getIdentifier();
}
@@ -76,7 +78,7 @@
return getCompoundIdentifer() + "." + action.name();
}
- BigDecimal getInterest() {
+ public BigDecimal getInterest() {
return customerCase.getInterest();
}
-}
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/DataContextService.java b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextService.java
index 388ae0c..20f2e0d 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/DataContextService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextService.java
@@ -66,6 +66,10 @@
"Individual loan not found ''{0}.{1}''.",
productIdentifier, caseIdentifier));
- return new DataContextOfAction(product, customerCase, caseParameters, oneTimeAccountAssignments);
+ return new DataContextOfAction(
+ product,
+ customerCase,
+ caseParameters,
+ oneTimeAccountAssignments);
}
-}
+}
\ No newline at end of file
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..fbc0273 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
@@ -16,16 +16,19 @@
package io.mifos.individuallending.internal.service;
import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.IndividualLendingPatternFactory;
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
+import io.mifos.portfolio.api.v1.domain.RequiredAccountAssignment;
import io.mifos.portfolio.service.internal.mapper.CaseMapper;
import io.mifos.portfolio.service.internal.mapper.ProductMapper;
import io.mifos.portfolio.service.internal.repository.CaseAccountAssignmentEntity;
import io.mifos.portfolio.service.internal.repository.ProductAccountAssignmentEntity;
import javax.annotation.Nonnull;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@@ -37,9 +40,19 @@
private final @Nonnull List<AccountAssignment> oneTimeAccountAssignments;
public DesignatorToAccountIdentifierMapper(final @Nonnull DataContextOfAction dataContextOfAction) {
- this.productAccountAssignments = dataContextOfAction.getProductEntity().getAccountAssignments();
- this.caseAccountAssignments = dataContextOfAction.getCustomerCaseEntity().getAccountAssignments();
- this.oneTimeAccountAssignments = dataContextOfAction.getOneTimeAccountAssignments();
+ this(dataContextOfAction.getProductEntity().getAccountAssignments(),
+ dataContextOfAction.getCustomerCaseEntity().getAccountAssignments(),
+ dataContextOfAction.getOneTimeAccountAssignments());
+ }
+
+ DesignatorToAccountIdentifierMapper(
+ final @Nonnull Set<ProductAccountAssignmentEntity> productAccountAssignments,
+ final @Nonnull Set<CaseAccountAssignmentEntity> caseAccountAssignments,
+ final @Nonnull List<AccountAssignment> oneTimeAccountAssignments) {
+
+ this.productAccountAssignments = productAccountAssignments;
+ this.caseAccountAssignments = caseAccountAssignments;
+ this.oneTimeAccountAssignments = oneTimeAccountAssignments;
}
private Stream<AccountAssignment> allAccountAssignmentsAsStream() {
@@ -48,20 +61,147 @@
private Stream<AccountAssignment> fixedAccountAssignmentsAsStream() {
return Stream.concat(caseAccountAssignments.stream().map(CaseMapper::mapAccountAssignmentEntity),
- productAccountAssignments.stream().map(ProductMapper::mapAccountAssignmentEntity));
+ productAccountAssignmentsAsStream());
+ }
+
+ private Stream<AccountAssignment> productAccountAssignmentsAsStream() {
+ return productAccountAssignments.stream().map(ProductMapper::mapAccountAssignmentEntity);
+ }
+
+ private Optional<AccountAssignment> mapToAccountAssignment(final @Nonnull String accountDesignator) {
+ return allAccountAssignmentsAsStream()
+ .filter(x -> x.getDesignator().equals(accountDesignator))
+ .findFirst();
+ }
+
+ private Optional<AccountAssignment> mapToProductAccountAssignment(final @Nonnull String accountDesignator) {
+ return productAccountAssignments.stream().map(ProductMapper::mapAccountAssignmentEntity)
+ .filter(x -> x.getDesignator().equals(accountDesignator))
+ .findFirst();
+ }
+
+ Optional<AccountAssignment> mapToCaseAccountAssignment(final @Nonnull String accountDesignator) {
+ return caseAccountAssignments.stream().map(CaseMapper::mapAccountAssignmentEntity)
+ .filter(x -> x.getDesignator().equals(accountDesignator))
+ .findFirst();
+ }
+
+ public Optional<String> map(final @Nonnull String accountDesignator) {
+ final Set<String> accountAssignmentGroups = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentGroups();
+ if (accountAssignmentGroups.contains(accountDesignator))
+ return Optional.empty();
+ return mapToAccountAssignment(accountDesignator)
+ .map(AccountAssignment::getAccountIdentifier);
}
public String mapOrThrow(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));
+ return map(accountDesignator).orElseThrow(() ->
+ ServiceException.badRequest("A required account designator was not set ''{0}''.", accountDesignator));
+ }
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ public static class GroupNeedingLedger {
+ final String groupName;
+ final String parentLedger;
+
+ GroupNeedingLedger(final String groupName, final String parentLedger) {
+ this.groupName = groupName;
+ this.parentLedger = parentLedger;
+ }
+
+ public String getGroupName() {
+ return groupName;
+ }
+
+ public String getParentLedger() {
+ return parentLedger;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ GroupNeedingLedger that = (GroupNeedingLedger) o;
+ return Objects.equals(groupName, that.groupName) &&
+ Objects.equals(parentLedger, that.parentLedger);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(groupName, parentLedger);
+ }
+
+ @Override
+ public String toString() {
+ return "GroupNeedingLedger{" +
+ "groupName='" + groupName + '\'' +
+ ", parentLedger='" + parentLedger + '\'' +
+ '}';
+ }
+ }
+
+ public Stream<GroupNeedingLedger> getGroupsNeedingLedgers() {
+ //If all of the accounts in one group are assigned the same ledger, create a grouping ledger at the case level for
+ // those accounts under that ledger.
+ //Save that grouping ledger to an account assignment using the group name as its designator.
+ //To this end, return a stream of group names requiring a ledger, and the parent ledger under which the ledger
+ // should be created.
+
+ final Set<String> accountAssignmentGroups = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentGroups();
+ final Set<RequiredAccountAssignment> accountAssignmentsRequired = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentsRequired();
+
+ return accountAssignmentGroups.stream()
+ .filter(groupName -> !mapToProductAccountAssignment(groupName).isPresent()) //Only assign groups to ledgers which aren't already assigned.
+ .map(groupName -> {
+ final Stream<RequiredAccountAssignment> requiredAccountAssignmentsInThisGroup
+ = accountAssignmentsRequired.stream().filter(x -> groupName.equals(x.getGroup()));
+ final List<String> ledgersAssignedToThem = requiredAccountAssignmentsInThisGroup
+ .map(requiredAccountAssignment -> mapToProductAccountAssignment(requiredAccountAssignment.getAccountDesignator()))
+ .map(optionalAccountAssignment -> optionalAccountAssignment.map(AccountAssignment::getLedgerIdentifier))
+ .distinct()
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .limit(2) //If there's more than one then we won't be creating this ledger. We don't care about more than two.
+ .collect(Collectors.toList());
+ if (ledgersAssignedToThem.size() == 1) {
+ //noinspection ConstantConditions
+ return new GroupNeedingLedger(groupName, ledgersAssignedToThem.get(0));
+ }
+ else
+ return null;
+ })
+ .filter(Objects::nonNull);
}
public Stream<AccountAssignment> getLedgersNeedingAccounts() {
- return fixedAccountAssignmentsAsStream()
- .filter(x -> !x.getDesignator().equals(AccountDesignators.ENTRY))
- .filter(x -> (x.getAccountIdentifier() == null) && (x.getLedgerIdentifier() != null));
+ final Set<String> accountAssignmentGroups = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentGroups();
+ final Set<RequiredAccountAssignment> accountAssignmentsRequired = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentsRequired();
+ final Map<String, RequiredAccountAssignment> accountAssignmentsRequiredMap = accountAssignmentsRequired.stream().collect(Collectors.toMap(RequiredAccountAssignment::getAccountDesignator, x -> x));
+ final Map<String, Optional<String>> groupToLedgerMapping = accountAssignmentGroups.stream()
+ .collect(Collectors.toMap(
+ Function.identity(),
+ group -> mapToCaseAccountAssignment(group).map(AccountAssignment::getAccountIdentifier)));
+
+ final Stream<AccountAssignment> ledgerAccountAssignments = productAccountAssignmentsAsStream()
+ .filter(x -> !x.getDesignator().equals(AccountDesignators.ENTRY))
+ .filter(x -> (x.getAccountIdentifier() == null) && (x.getLedgerIdentifier() != null));
+
+ return ledgerAccountAssignments
+ .map(ledgerAccountAssignment -> {
+ final String accountAssignmentGroup = accountAssignmentsRequiredMap.get(ledgerAccountAssignment.getDesignator()).getGroup();
+ if (accountAssignmentGroup == null)
+ return ledgerAccountAssignment;
+ else {
+ final Optional<String> changedLedger = groupToLedgerMapping.get(accountAssignmentGroup);
+ if (!changedLedger.isPresent())
+ return ledgerAccountAssignment;
+ else {
+ final AccountAssignment ret = new AccountAssignment();
+ ret.setDesignator(ledgerAccountAssignment.getDesignator());
+ ret.setLedgerIdentifier(changedLedger.get());
+ return ret;
+ }
+ }
+ });
}
}
\ No newline at end of file
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..9ae6402 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
@@ -20,6 +20,10 @@
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.service.costcomponent.CostComponentService;
+import io.mifos.individuallending.internal.service.costcomponent.PaymentBuilder;
+import io.mifos.individuallending.internal.service.costcomponent.SimulatedRunningBalances;
+import io.mifos.individuallending.internal.service.schedule.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -51,12 +55,13 @@
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(initialDisbursalDate, dataContextOfAction.getCaseParameters());
- final Set<Action> actionsScheduled = scheduledActions.stream().map(x -> x.action).collect(Collectors.toSet());
+ final Set<Action> actionsScheduled = scheduledActions.stream().map(ScheduledAction::getAction).collect(Collectors.toSet());
final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(dataContextOfAction.getProductEntity().getIdentifier(), scheduledActions);
final BigDecimal loanPaymentSize = CostComponentService.getLoanPaymentSize(
dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum(),
+ dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum(),
dataContextOfAction.getInterest(),
minorCurrencyUnitDigits,
scheduledCharges);
@@ -123,41 +128,42 @@
.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.multiply(BigDecimal.valueOf(2));
+ 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);
+ true);
- 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, repaymentPeriod.getEndDate()));
}
return plannedPayments;
}
@@ -172,9 +178,9 @@
private static Period getPeriodFromScheduledCharge(final ScheduledCharge scheduledCharge) {
final ScheduledAction scheduledAction = scheduledCharge.getScheduledAction();
- if (ScheduledActionHelpers.actionHasNoActionPeriod(scheduledAction.action))
+ if (ScheduledActionHelpers.actionHasNoActionPeriod(scheduledAction.getAction()))
return new Period(null, null);
else
- return scheduledAction.repaymentPeriod;
+ return scheduledAction.getRepaymentPeriod();
}
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/LossProvisionStepService.java b/service/src/main/java/io/mifos/individuallending/internal/service/LossProvisionStepService.java
new file mode 100644
index 0000000..d07df7d
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/LossProvisionStepService.java
@@ -0,0 +1,73 @@
+/*
+ * 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.core.lang.ServiceException;
+import io.mifos.individuallending.api.v1.domain.product.LossProvisionStep;
+import io.mifos.individuallending.internal.mapper.LossProvisionStepMapper;
+import io.mifos.individuallending.internal.repository.LossProvisionStepRepository;
+import io.mifos.portfolio.service.internal.repository.ProductRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class LossProvisionStepService {
+ private final static List<LossProvisionStep> DEFAULT_LOSS_PROVISION_STEPS = Arrays.asList(
+ new LossProvisionStep(0, BigDecimal.ONE),
+ new LossProvisionStep(1, BigDecimal.valueOf(9)),
+ new LossProvisionStep(30, BigDecimal.valueOf(30)),
+ new LossProvisionStep(60, BigDecimal.valueOf(60)));
+
+ private final ProductRepository productRepository;
+ private final LossProvisionStepRepository lossProvisionStepRepository;
+
+ @Autowired
+ public LossProvisionStepService(
+ final ProductRepository productRepository,
+ final LossProvisionStepRepository lossProvisionStepRepository) {
+ this.productRepository = productRepository;
+ this.lossProvisionStepRepository = lossProvisionStepRepository;
+ }
+
+ public Optional<LossProvisionStep> findByProductIdAndDaysLate(
+ final Long id,
+ final int daysLate) {
+ return lossProvisionStepRepository.findByProductIdAndDaysLate(id, daysLate).map(LossProvisionStepMapper::map);
+ }
+
+ public List<LossProvisionStep> findByProductIdentifier(
+ final String productIdentifier) {
+ final Long productId = productRepository.findByIdentifier(productIdentifier)
+ .orElseThrow(() -> ServiceException.notFound("Product ''{}'' doesn''t exist.", productIdentifier))
+ .getId();
+ final List<LossProvisionStep> ret = lossProvisionStepRepository.findByProductId(productId)
+ .map(LossProvisionStepMapper::map)
+ .collect(Collectors.toList());
+ if (!ret.isEmpty())
+ return ret;
+ else
+ return DEFAULT_LOSS_PROVISION_STEPS;
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/RateCollectors.java b/service/src/main/java/io/mifos/individuallending/internal/service/RateCollectors.java
index fd65ec3..6e4cf09 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/RateCollectors.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/RateCollectors.java
@@ -21,11 +21,11 @@
/**
* @author Myrle Krantz
*/
-final class RateCollectors {
+public final class RateCollectors {
private RateCollectors() {}
- static Collector<BigDecimal, ?, BigDecimal> compound(int significantDigits)
+ public static Collector<BigDecimal, ?, BigDecimal> compound(int significantDigits)
{
return Collector.of(
() -> new Compound(significantDigits),
@@ -34,7 +34,7 @@
Compound::finish);
}
- static Collector<BigDecimal, ?, BigDecimal> geometricMean(int significantDigits)
+ public static Collector<BigDecimal, ?, BigDecimal> geometricMean(int significantDigits)
{
return Collector.of(
() -> new GeometricMean(significantDigits),
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderService.java
new file mode 100644
index 0000000..d3bed9b
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderService.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledActionHelpers;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class AcceptPaymentBuilderService implements PaymentBuilderService {
+ private final ScheduledChargesService scheduledChargesService;
+
+ @Autowired
+ public AcceptPaymentBuilderService(
+ final ScheduledChargesService scheduledChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
+ }
+
+ @Override
+ public PaymentBuilder getPaymentBuilder(
+ final DataContextOfAction dataContextOfAction,
+ final BigDecimal requestedLoanPaymentSize,
+ final LocalDate forDate,
+ final RunningBalances runningBalances) {
+ final LocalDateTime startOfTerm = runningBalances.getStartOfTermOrThrow(dataContextOfAction);
+
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+ final ScheduledAction scheduledAction
+ = ScheduledActionHelpers.getNextScheduledPayment(
+ startOfTerm.toLocalDate(),
+ forDate,
+ dataContextOfAction.getCustomerCaseEntity().getEndOfTerm().toLocalDate(),
+ dataContextOfAction.getCaseParameters()
+ );
+
+ final List<ScheduledCharge> scheduledChargesForThisAction = scheduledChargesService.getScheduledCharges(
+ productIdentifier,
+ Collections.singletonList(scheduledAction));
+
+ final BigDecimal loanPaymentSize;
+
+ if (requestedLoanPaymentSize != null) {
+ loanPaymentSize = requestedLoanPaymentSize
+ .min(runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP));
+ }
+ else if (scheduledAction.getActionPeriod() != null && scheduledAction.getActionPeriod().isLastPeriod()) {
+ loanPaymentSize = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP);
+ }
+ else {
+ loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize()
+ .min(runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP));
+ }
+
+
+ return CostComponentService.getCostComponentsForScheduledCharges(
+ scheduledChargesForThisAction,
+ caseParameters.getBalanceRangeMaximum(),
+ runningBalances,
+ dataContextOfAction.getCaseParametersEntity().getPaymentSize(),
+ BigDecimal.ZERO,
+ loanPaymentSize,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderService.java
new file mode 100644
index 0000000..ad723f8
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderService.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.Period;
+import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class ApplyInterestPaymentBuilderService implements PaymentBuilderService {
+ private final ScheduledChargesService scheduledChargesService;
+
+ @Autowired
+ public ApplyInterestPaymentBuilderService(final ScheduledChargesService scheduledChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
+ }
+
+ @Override
+ public PaymentBuilder getPaymentBuilder(
+ final DataContextOfAction dataContextOfAction,
+ final BigDecimal ignored,
+ final LocalDate forDate,
+ final RunningBalances runningBalances)
+ {
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+ final ScheduledAction interestAction = new ScheduledAction(Action.APPLY_INTEREST, forDate, new Period(1, forDate));
+
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
+ productIdentifier,
+ Collections.singletonList(interestAction));
+
+ return CostComponentService.getCostComponentsForScheduledCharges(
+ scheduledCharges,
+ caseParameters.getBalanceRangeMaximum(),
+ runningBalances,
+ dataContextOfAction.getCaseParametersEntity().getPaymentSize(),
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ApprovePaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ApprovePaymentBuilderService.java
new file mode 100644
index 0000000..47afb7d
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ApprovePaymentBuilderService.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class ApprovePaymentBuilderService implements PaymentBuilderService {
+ private final ScheduledChargesService scheduledChargesService;
+
+ @Autowired
+ public ApprovePaymentBuilderService(
+ final ScheduledChargesService scheduledChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
+ }
+
+ @Override
+ public PaymentBuilder getPaymentBuilder(
+ final DataContextOfAction dataContextOfAction,
+ final BigDecimal ignored,
+ final LocalDate forDate,
+ final RunningBalances runningBalances)
+ {
+ //Charge the approval fee if applicable.
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+ final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.APPROVE, forDate));
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
+ productIdentifier, scheduledActions);
+
+ return CostComponentService.getCostComponentsForScheduledCharges(
+ scheduledCharges,
+ caseParameters.getBalanceRangeMaximum(),
+ new SimulatedRunningBalances(),
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ClosePaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ClosePaymentBuilderService.java
new file mode 100644
index 0000000..1b37c95
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/ClosePaymentBuilderService.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.Period;
+import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class ClosePaymentBuilderService implements PaymentBuilderService {
+ private final ScheduledChargesService scheduledChargesService;
+
+ @Autowired
+ public ClosePaymentBuilderService(
+ final ScheduledChargesService scheduledChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
+ }
+
+ @Override
+ public PaymentBuilder getPaymentBuilder(
+ final DataContextOfAction dataContextOfAction,
+ final BigDecimal ignored,
+ final LocalDate forDate,
+ final RunningBalances runningBalances)
+ {
+ if (runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).compareTo(BigDecimal.ZERO) != 0)
+ throw ServiceException.conflict("Cannot close loan until the balance is zero.");
+
+
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+ final ScheduledAction closeAction = new ScheduledAction(Action.CLOSE, forDate, new Period(1, forDate));
+
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
+ productIdentifier,
+ Collections.singletonList(closeAction));
+
+ return CostComponentService.getCostComponentsForScheduledCharges(
+ scheduledCharges,
+ caseParameters.getBalanceRangeMaximum(),
+ runningBalances,
+ dataContextOfAction.getCaseParametersEntity().getPaymentSize(),
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentService.java
new file mode 100644
index 0000000..6d7b0ca
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentService.java
@@ -0,0 +1,235 @@
+/*
+ * 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.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.service.AnnuityPayment;
+import io.mifos.individuallending.internal.service.RateCollectors;
+import io.mifos.individuallending.internal.service.schedule.Period;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import org.javamoney.calc.common.Rate;
+import org.javamoney.moneta.Money;
+
+import javax.money.MonetaryAmount;
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.LocalDate;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * @author Myrle Krantz
+ */
+public class CostComponentService {
+ private static final int EXTRA_PRECISION = 4;
+ private static final int RUNNING_CALCULATION_PRECISION = 8;
+
+ public static PaymentBuilder getCostComponentsForScheduledCharges(
+ final Collection<ScheduledCharge> scheduledCharges,
+ final BigDecimal maximumBalance,
+ final RunningBalances preChargeBalances,
+ final BigDecimal contractualRepayment,
+ final BigDecimal requestedDisbursement,
+ final BigDecimal requestedRepayment,
+ final BigDecimal percentPoints,
+ final int minorCurrencyUnitDigits,
+ final boolean accrualAccounting) {
+ final PaymentBuilder paymentBuilder = new PaymentBuilder(preChargeBalances, accrualAccounting);
+
+ for (final ScheduledCharge scheduledCharge : scheduledCharges) {
+ if (accrualAccounting || !isAccrualChargeForAction(scheduledCharge.getChargeDefinition(), scheduledCharge.getScheduledAction().getAction())) {
+ final BigDecimal chargeAmount;
+ if (!isIncurralActionForAccruedCharge(scheduledCharge.getChargeDefinition(), scheduledCharge.getScheduledAction().getAction()))
+ {
+ final BigDecimal amountProportionalTo = getAmountProportionalTo(
+ scheduledCharge,
+ maximumBalance,
+ preChargeBalances,
+ contractualRepayment,
+ requestedDisbursement,
+ requestedRepayment,
+ paymentBuilder);
+ if (scheduledCharge.getChargeRange().map(x ->
+ !x.amountIsWithinRange(amountProportionalTo)).orElse(false))
+ continue;
+
+ chargeAmount = howToApplyScheduledChargeToAmount(scheduledCharge, percentPoints)
+ .apply(amountProportionalTo)
+ .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
+ }
+ else
+ {
+ chargeAmount = preChargeBalances.getAccruedBalanceForCharge(scheduledCharge.getChargeDefinition())
+ .add(paymentBuilder.getBalanceAdjustment(scheduledCharge.getChargeDefinition().getAccrualAccountDesignator()));
+ }
+
+ paymentBuilder.adjustBalances(
+ scheduledCharge.getScheduledAction().getAction(),
+ scheduledCharge.getChargeDefinition(),
+ chargeAmount);
+ }
+ }
+
+ return paymentBuilder;
+ }
+
+ private static BigDecimal getAmountProportionalTo(
+ final ScheduledCharge scheduledCharge,
+ final BigDecimal maximumBalance,
+ 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(
+ scheduledCharge,
+ chargeProportionalTo,
+ maximumBalance,
+ runningBalances,
+ contractualRepayment,
+ requestedDisbursement,
+ requestedRepayment,
+ paymentBuilder))
+ .orElse(BigDecimal.ZERO);
+ }
+
+ static BigDecimal getAmountProportionalTo(
+ final ScheduledCharge scheduledCharge,
+ final ChargeProportionalDesignator chargeProportionalTo,
+ final BigDecimal maximumBalance,
+ final RunningBalances runningBalances,
+ final BigDecimal contractualRepayment,
+ final BigDecimal requestedDisbursement,
+ final BigDecimal requestedRepayment,
+ final PaymentBuilder paymentBuilder) {
+ switch (chargeProportionalTo) {
+ case NOT_PROPORTIONAL:
+ return BigDecimal.ONE;
+ case MAXIMUM_BALANCE_DESIGNATOR:
+ return maximumBalance;
+ case RUNNING_BALANCE_DESIGNATOR: {
+ final BigDecimal customerLoanRunningBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP);
+ return customerLoanRunningBalance.subtract(paymentBuilder.getBalanceAdjustment(AccountDesignators.CUSTOMER_LOAN_GROUP));
+ }
+ case PRINCIPAL_DESIGNATOR: {
+ return runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ }
+ case CONTRACTUAL_REPAYMENT_DESIGNATOR:
+ return contractualRepayment;
+ case REQUESTED_DISBURSEMENT_DESIGNATOR:
+ return requestedDisbursement;
+ case REQUESTED_REPAYMENT_DESIGNATOR:
+ return requestedRepayment.add(paymentBuilder.getBalanceAdjustment(AccountDesignators.ENTRY));
+ case TO_ACCOUNT_DESIGNATOR:
+ return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getToAccountDesignator())
+ .subtract(paymentBuilder.getBalanceAdjustment(scheduledCharge.getChargeDefinition().getToAccountDesignator()));
+ case FROM_ACCOUNT_DESIGNATOR:
+ return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getFromAccountDesignator())
+ .add(paymentBuilder.getBalanceAdjustment(scheduledCharge.getChargeDefinition().getFromAccountDesignator()));
+ default:
+ return BigDecimal.ZERO;
+ }
+ }
+
+ private static Function<BigDecimal, BigDecimal> howToApplyScheduledChargeToAmount(
+ final ScheduledCharge scheduledCharge, final BigDecimal percentPoints)
+ {
+ switch (scheduledCharge.getChargeDefinition().getChargeMethod())
+ {
+ case FIXED: {
+ return (amountProportionalTo) -> scheduledCharge.getChargeDefinition().getAmount();
+ }
+ case PROPORTIONAL: {
+ final BigDecimal chargeAmountPerPeriod = PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, scheduledCharge.getChargeDefinition().getAmount(), RUNNING_CALCULATION_PRECISION);
+ return chargeAmountPerPeriod::multiply;
+ }
+ case INTEREST: {
+ final BigDecimal chargeAmountPerPeriod = PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, percentPoints, RUNNING_CALCULATION_PRECISION);
+ return chargeAmountPerPeriod::multiply;
+ }
+ default: {
+ return (amountProportionalTo) -> BigDecimal.ZERO;
+ }
+ }
+ }
+
+ public static BigDecimal getLoanPaymentSize(
+ final BigDecimal maximumBalanceSize,
+ final BigDecimal disbursementSize,
+ final BigDecimal interest,
+ final int minorCurrencyUnitDigits,
+ final List<ScheduledCharge> scheduledCharges) {
+ final int precision = disbursementSize.precision() - disbursementSize.scale() + minorCurrencyUnitDigits + EXTRA_PRECISION;
+ final Map<Period, BigDecimal> accrualRatesByPeriod
+ = PeriodChargeCalculator.getPeriodAccrualInterestRate(interest, scheduledCharges, precision);
+
+ final int periodCount = accrualRatesByPeriod.size();
+ if (periodCount == 0)
+ return disbursementSize;
+
+ final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream()
+ .collect(RateCollectors.geometricMean(precision));
+
+ final List<ScheduledCharge> disbursementFees = scheduledCharges.stream()
+ .filter(x -> x.getScheduledAction().getAction().equals(Action.DISBURSE))
+ .collect(Collectors.toList());
+ final PaymentBuilder paymentBuilder = getCostComponentsForScheduledCharges(
+ 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_PRINCIPAL,
+ AccountDesignators.CUSTOMER_LOAN_FEES).negate();
+
+ final MonetaryAmount presentValue = AnnuityPayment.calculate(
+ Money.of(finalDisbursementSize, "XXX"),
+ Rate.of(geometricMeanAccrualRate),
+ periodCount);
+ return BigDecimal.valueOf(presentValue.getNumber().doubleValueExact()).setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
+ }
+
+ private static boolean isIncurralActionForAccruedCharge(final ChargeDefinition chargeDefinition, final Action action) {
+ return chargeDefinition.getAccrueAction() != null &&
+ chargeDefinition.getChargeAction().equals(action.name());
+ }
+
+ private static boolean isAccrualChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
+ return chargeDefinition.getAccrueAction() != null &&
+ chargeDefinition.getAccrueAction().equals(action.name());
+ }
+
+ public static LocalDate today() {
+ return LocalDate.now(Clock.systemUTC());
+ }
+
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DenyPaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DenyPaymentBuilderService.java
new file mode 100644
index 0000000..5bf278f
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DenyPaymentBuilderService.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class DenyPaymentBuilderService implements PaymentBuilderService {
+ private final ScheduledChargesService scheduledChargesService;
+
+ @Autowired
+ public DenyPaymentBuilderService(
+ final ScheduledChargesService scheduledChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
+ }
+
+ @Override
+ public PaymentBuilder getPaymentBuilder(
+ final DataContextOfAction dataContextOfAction,
+ final BigDecimal ignored,
+ final LocalDate forDate,
+ final RunningBalances runningBalances)
+ {
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+ final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.DENY, forDate));
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
+ productIdentifier, scheduledActions);
+
+ return CostComponentService.getCostComponentsForScheduledCharges(
+ scheduledCharges,
+ caseParameters.getBalanceRangeMaximum(),
+ new SimulatedRunningBalances(),
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderService.java
new file mode 100644
index 0000000..c843ee0
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderService.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class DisbursePaymentBuilderService implements PaymentBuilderService {
+ private final ScheduledChargesService scheduledChargesService;
+ private final LossProvisionChargesService lossProvisionChargesService;
+
+ @Autowired
+ public DisbursePaymentBuilderService(
+ final ScheduledChargesService scheduledChargesService,
+ final LossProvisionChargesService lossProvisionChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
+ this.lossProvisionChargesService = lossProvisionChargesService;
+ }
+
+ @Override
+ public PaymentBuilder getPaymentBuilder(
+ final @Nonnull DataContextOfAction dataContextOfAction,
+ final @Nullable BigDecimal requestedDisbursalSize,
+ final LocalDate forDate,
+ final RunningBalances runningBalances)
+ {
+ final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+
+ if (requestedDisbursalSize != null &&
+ dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum().compareTo(
+ currentBalance.add(requestedDisbursalSize)) < 0)
+ throw ServiceException.conflict("Cannot disburse over the maximum balance.");
+
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+ final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.DISBURSE, forDate));
+
+ final BigDecimal disbursalSize;
+ if (requestedDisbursalSize == null)
+ disbursalSize = dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum();
+ else
+ disbursalSize = requestedDisbursalSize;
+
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
+ productIdentifier, scheduledActions);
+ final Optional<ScheduledCharge> initialLossProvisionCharge = lossProvisionChargesService.getScheduledChargeForDisbursement(
+ dataContextOfAction, forDate);
+ initialLossProvisionCharge.ifPresent(scheduledCharges::add);
+
+ return CostComponentService.getCostComponentsForScheduledCharges(
+ scheduledCharges,
+ caseParameters.getBalanceRangeMaximum(),
+ runningBalances,
+ dataContextOfAction.getCaseParametersEntity().getPaymentSize(),
+ disbursalSize,
+ BigDecimal.ZERO,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
+ }
+
+ public BigDecimal getLoanPaymentSizeForSingleDisbursement(
+ final BigDecimal disbursementSize,
+ final DataContextOfAction dataContextOfAction) {
+ final List<ScheduledAction> hypotheticalScheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(
+ CostComponentService.today(),
+ dataContextOfAction.getCaseParameters());
+ final List<ScheduledCharge> hypotheticalScheduledCharges = scheduledChargesService.getScheduledCharges(
+ dataContextOfAction.getProductEntity().getIdentifier(),
+ hypotheticalScheduledActions);
+ return CostComponentService.getLoanPaymentSize(
+ disbursementSize,
+ disbursementSize,
+ dataContextOfAction.getInterest(),
+ dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits(),
+ hypotheticalScheduledCharges);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/MarkLatePaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/MarkLatePaymentBuilderService.java
new file mode 100644
index 0000000..c02e10e
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/MarkLatePaymentBuilderService.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class MarkLatePaymentBuilderService implements PaymentBuilderService {
+ private final ScheduledChargesService scheduledChargesService;
+
+ @Autowired
+ public MarkLatePaymentBuilderService(final ScheduledChargesService scheduledChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
+ }
+
+ @Override
+ public PaymentBuilder getPaymentBuilder(
+ final @Nonnull DataContextOfAction dataContextOfAction,
+ final @Nullable BigDecimal ignored,
+ final LocalDate forDate,
+ final RunningBalances runningBalances)
+ {
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+ final ScheduledAction scheduledAction = new ScheduledAction(Action.MARK_LATE, forDate);
+
+ final BigDecimal loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize();
+
+ final List<ScheduledCharge> scheduledChargesForThisAction = scheduledChargesService.getScheduledCharges(
+ productIdentifier,
+ Collections.singletonList(scheduledAction));
+
+ return CostComponentService.getCostComponentsForScheduledCharges(
+ scheduledChargesForThisAction,
+ caseParameters.getBalanceRangeMaximum(),
+ runningBalances,
+ loanPaymentSize,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/OpenPaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/OpenPaymentBuilderService.java
new file mode 100644
index 0000000..ec2cde7
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/OpenPaymentBuilderService.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class OpenPaymentBuilderService implements PaymentBuilderService {
+ private final ScheduledChargesService scheduledChargesService;
+
+ @Autowired
+ public OpenPaymentBuilderService(
+ final ScheduledChargesService scheduledChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
+ }
+
+ public PaymentBuilder getPaymentBuilder(
+ final DataContextOfAction dataContextOfAction,
+ final BigDecimal ignored,
+ final LocalDate forDate,
+ final RunningBalances runningBalances)
+ {
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+ final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.OPEN, forDate));
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
+ productIdentifier, scheduledActions);
+
+ return CostComponentService.getCostComponentsForScheduledCharges(
+ scheduledCharges,
+ caseParameters.getBalanceRangeMaximum(),
+ new SimulatedRunningBalances(),
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilder.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilder.java
new file mode 100644
index 0000000..4c2d646
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilder.java
@@ -0,0 +1,235 @@
+/*
+ * 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.costcomponent;
+
+import com.google.common.collect.Sets;
+import io.mifos.core.lang.DateConverter;
+import io.mifos.individuallending.IndividualLendingPatternFactory;
+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.api.v1.domain.RequiredAccountAssignment;
+
+import javax.annotation.Nullable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+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;
+ }
+
+ private Map<String, BigDecimal> copyBalanceAdjustments() {
+ return balanceAdjustments.entrySet().stream()
+ .filter(x -> x.getValue().compareTo(BigDecimal.ZERO) != 0)
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ }
+
+ public Payment buildPayment(
+ final Action action,
+ final Set<String> forAccountDesignators,
+ final @Nullable LocalDate forDate)
+ {
+ 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());
+
+ final Payment ret = new Payment(costComponentList, copyBalanceAdjustments());
+ ret.setDate(forDate == null ? null : DateConverter.toIsoString(forDate.atStartOfDay()));
+ return ret;
+ }
+ else {
+ return buildPayment(forDate);
+ }
+
+ }
+
+ private Payment buildPayment(final @Nullable LocalDate forDate) {
+ 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());
+
+ final Payment ret = new Payment(costComponentList, copyBalanceAdjustments());
+ ret.setDate(forDate == null ? null : DateConverter.toIsoString(forDate.atStartOfDay()));
+ return ret;
+ }
+
+ public PlannedPayment accumulatePlannedPayment(
+ final SimulatedRunningBalances balances,
+ final @Nullable LocalDate forDate) {
+ final Payment payment = buildPayment(forDate);
+ balanceAdjustments.forEach(balances::adjustBalance);
+ final Map<String, BigDecimal> balancesCopy = balances.snapshot();
+
+ return new PlannedPayment(payment, balancesCopy);
+ }
+
+ public Map<String, BigDecimal> getBalanceAdjustments() {
+ return balanceAdjustments;
+ }
+
+ public BigDecimal getBalanceAdjustment(final String... accountDesignators) {
+ return Arrays.stream(accountDesignators)
+ .map(accountDesignator -> balanceAdjustments.getOrDefault(accountDesignator, BigDecimal.ZERO))
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+ void adjustBalances(
+ final Action action,
+ final ChargeDefinition chargeDefinition,
+ final BigDecimal chargeAmount) {
+ BigDecimal adjustedChargeAmount;
+ if (this.accrualAccounting && chargeIsAccrued(chargeDefinition)) {
+ if (Action.valueOf(chargeDefinition.getAccrueAction()) == action) {
+ adjustedChargeAmount = getMaxCharge(chargeDefinition.getFromAccountDesignator(), chargeDefinition.getAccrualAccountDesignator(), chargeAmount);
+
+ this.addToBalance(chargeDefinition.getFromAccountDesignator(), adjustedChargeAmount.negate());
+ this.addToBalance(chargeDefinition.getAccrualAccountDesignator(), adjustedChargeAmount);
+ } else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
+ adjustedChargeAmount = getMaxCharge(chargeDefinition.getAccrualAccountDesignator(), chargeDefinition.getToAccountDesignator(), chargeAmount);
+
+ this.addToBalance(chargeDefinition.getAccrualAccountDesignator(), adjustedChargeAmount.negate());
+ this.addToBalance(chargeDefinition.getToAccountDesignator(), adjustedChargeAmount);
+
+ addToCostComponent(chargeDefinition, adjustedChargeAmount);
+ }
+ }
+ else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
+ adjustedChargeAmount = getMaxCharge(chargeDefinition.getFromAccountDesignator(), chargeDefinition.getToAccountDesignator(), chargeAmount);
+
+ this.addToBalance(chargeDefinition.getFromAccountDesignator(), adjustedChargeAmount.negate());
+ this.addToBalance(chargeDefinition.getToAccountDesignator(), adjustedChargeAmount);
+
+ addToCostComponent(chargeDefinition, adjustedChargeAmount);
+ }
+ }
+
+ private BigDecimal getMaxCharge(
+ final String fromAccountDesignator,
+ final String toAccountDesignator,
+ final BigDecimal plannedCharge) {
+ final BigDecimal expectedImpactOnDebitAccount = plannedCharge.subtract(this.getBalanceAdjustment(fromAccountDesignator));
+ final BigDecimal maxImpactOnDebitAccount = prePaymentBalances.getMaxDebit(fromAccountDesignator, expectedImpactOnDebitAccount);
+ final BigDecimal maxDebit = maxImpactOnDebitAccount.add(this.getBalanceAdjustment(fromAccountDesignator))
+ .max(BigDecimal.ZERO);
+
+ final BigDecimal expectedImpactOnCreditAccount = plannedCharge.add(this.getBalanceAdjustment(toAccountDesignator));
+ final BigDecimal maxImpactOnCreditAccount = prePaymentBalances.getMaxCredit(toAccountDesignator, expectedImpactOnCreditAccount);
+ final BigDecimal maxCredit = maxImpactOnCreditAccount.subtract(this.getBalanceAdjustment(toAccountDesignator))
+ .max(BigDecimal.ZERO);
+ return maxCredit.min(maxDebit);
+ }
+
+ private static boolean chargeIsAccrued(final ChargeDefinition chargeDefinition) {
+ return chargeDefinition.getAccrualAccountDesignator() != null;
+ }
+
+ private 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);
+ }
+
+ private 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());
+
+ final Set<String> expandedForAccountDesignators = expandAccountDesignators(forAccountDesignators);
+
+ return !Sets.intersection(accountsToCompare, expandedForAccountDesignators).isEmpty();
+ }
+
+ static Set<String> expandAccountDesignators(final Set<String> accountDesignators) {
+ final Set<RequiredAccountAssignment> accountAssignmentsRequired = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentsRequired();
+ final Map<String, List<RequiredAccountAssignment>> accountAssignmentsByGroup = accountAssignmentsRequired.stream()
+ .filter(x -> x.getGroup() != null)
+ .collect(Collectors.groupingBy(RequiredAccountAssignment::getGroup, Collectors.toList()));
+ final Set<String> groupExpansions = accountDesignators.stream()
+ .flatMap(accountDesignator -> {
+ final List<RequiredAccountAssignment> group = accountAssignmentsByGroup.get(accountDesignator);
+ if (group != null)
+ return group.stream();
+ else
+ return Stream.empty();
+ })
+ .map(RequiredAccountAssignment::getAccountDesignator)
+ .collect(Collectors.toSet());
+ final Set<String> ret = new HashSet<>(accountDesignators);
+ ret.addAll(groupExpansions);
+ return ret;
+ }
+
+ private static CostComponent constructEmptyCostComponent(final ChargeDefinition chargeDefinition) {
+ final CostComponent ret = new CostComponent();
+ ret.setChargeIdentifier(chargeDefinition.getIdentifier());
+ ret.setAmount(BigDecimal.ZERO);
+ return ret;
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderService.java
new file mode 100644
index 0000000..913982c
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderService.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+
+import javax.annotation.Nonnull;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+public interface PaymentBuilderService {
+
+ PaymentBuilder getPaymentBuilder(
+ final @Nonnull DataContextOfAction dataContextOfAction,
+ final BigDecimal forPaymentSize,
+ final LocalDate forDate,
+ final @Nonnull RunningBalances runningBalances);
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PeriodChargeCalculator.java
similarity index 71%
rename from service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
rename to service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PeriodChargeCalculator.java
index 3aaebf5..5c57218 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PeriodChargeCalculator.java
@@ -13,9 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.costcomponent;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.service.RateCollectors;
+import io.mifos.individuallending.internal.service.schedule.Period;
+import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledActionHelpers;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import java.math.BigDecimal;
@@ -36,9 +41,19 @@
final List<ScheduledCharge> scheduledCharges,
final int precision) {
return scheduledCharges.stream()
- .filter(PeriodChargeCalculator::accruedInterestCharge)
- .collect(Collectors.groupingBy(scheduledCharge -> scheduledCharge.getScheduledAction().repaymentPeriod,
- Collectors.mapping(x -> chargeAmountPerPeriod(x, interest, precision), RateCollectors.compound(precision))));
+ .filter(PeriodChargeCalculator::accruedInterestCharge)
+ .collect(Collectors.groupingBy(scheduledCharge -> scheduledCharge.getScheduledAction().getRepaymentPeriod(),
+ Collectors.mapping(x -> chargeAmountPerPeriod(x, interest, precision), Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
+ }
+
+ static Map<Period, BigDecimal> getPeriodAccrualCompoundedInterestRate(
+ final BigDecimal interest,
+ final List<ScheduledCharge> scheduledCharges,
+ final int precision) {
+ return scheduledCharges.stream()
+ .filter(PeriodChargeCalculator::accruedInterestCharge)
+ .collect(Collectors.groupingBy(scheduledCharge -> scheduledCharge.getScheduledAction().getRepaymentPeriod(),
+ Collectors.mapping(x -> chargeAmountPerPeriod(x, interest, precision), RateCollectors.compound(precision))));
}
private static boolean accruedInterestCharge(final ScheduledCharge scheduledCharge)
@@ -46,8 +61,8 @@
return scheduledCharge.getChargeDefinition().getAccrualAccountDesignator() != null &&
scheduledCharge.getChargeDefinition().getAccrueAction() != null &&
scheduledCharge.getChargeDefinition().getAccrueAction().equals(Action.APPLY_INTEREST.name()) &&
- scheduledCharge.getScheduledAction().action == Action.ACCEPT_PAYMENT &&
- scheduledCharge.getScheduledAction().actionPeriod != null &&
+ scheduledCharge.getScheduledAction().getAction() == Action.ACCEPT_PAYMENT &&
+ scheduledCharge.getScheduledAction().getActionPeriod() != null &&
scheduledCharge.getChargeDefinition().getChargeMethod() == ChargeDefinition.ChargeMethod.INTEREST;
}
@@ -65,7 +80,7 @@
final BigDecimal actionPeriodDuration
= BigDecimal.valueOf(
- scheduledAction.actionPeriod
+ scheduledAction.getActionPeriod()
.getDuration()
.getSeconds());
final Optional<BigDecimal> accrualPeriodDuration = Optional.ofNullable(chargeDefinition.getAccrueAction())
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java
new file mode 100644
index 0000000..40b5095
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.DesignatorToAccountIdentifierMapper;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.service.internal.util.AccountingAdapter;
+import net.jodah.expiringmap.ExpirationPolicy;
+import net.jodah.expiringmap.ExpiringMap;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Myrle Krantz
+ */
+public class RealRunningBalances implements RunningBalances {
+ private final AccountingAdapter accountingAdapter;
+ private final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper;
+ private final DataContextOfAction dataContextOfAction;
+ private final ExpiringMap<String, BigDecimal> realAccountBalanceCache;
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ private Optional<LocalDateTime> startOfTerm;
+
+ public RealRunningBalances(
+ final AccountingAdapter accountingAdapter,
+ final DataContextOfAction dataContextOfAction) {
+ this.accountingAdapter = accountingAdapter;
+ this.designatorToAccountIdentifierMapper =
+ new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ this.dataContextOfAction = dataContextOfAction;
+ this.realAccountBalanceCache = 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::getCurrentAccountBalance).orElse(BigDecimal.ZERO);
+ })
+ .build();
+ this.startOfTerm = Optional.empty();
+ }
+
+ @Override
+ public BigDecimal getAccountBalance(final String accountDesignator) {
+ return realAccountBalanceCache.get(accountDesignator);
+ }
+
+ @Override
+ public BigDecimal getAccruedBalanceForCharge(final ChargeDefinition chargeDefinition) {
+ final String accrualAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator());
+
+ final LocalDate startOfTermLocalDate = getStartOfTermOrThrow(dataContextOfAction).toLocalDate();
+
+ final BigDecimal amountAccrued = accountingAdapter.sumMatchingEntriesSinceDate(
+ accrualAccountIdentifier,
+ startOfTermLocalDate,
+ dataContextOfAction.getMessageForCharge(Action.valueOf(chargeDefinition.getAccrueAction())));
+ final BigDecimal amountApplied = accountingAdapter.sumMatchingEntriesSinceDate(
+ accrualAccountIdentifier,
+ startOfTermLocalDate,
+ dataContextOfAction.getMessageForCharge(Action.valueOf(chargeDefinition.getChargeAction())));
+ return amountAccrued.subtract(amountApplied);
+ }
+
+ @Override
+ public Optional<LocalDateTime> getStartOfTerm(final DataContextOfAction dataContextOfAction) {
+ if (!startOfTerm.isPresent()) {
+ final String customerLoanPrincipalAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+
+ this.startOfTerm = accountingAdapter.getDateOfOldestEntryContainingMessage(
+ customerLoanPrincipalAccountIdentifier,
+ dataContextOfAction.getMessageForCharge(Action.DISBURSE));
+ }
+
+ return this.startOfTerm;
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RecoverPaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RecoverPaymentBuilderService.java
new file mode 100644
index 0000000..f79fa28
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RecoverPaymentBuilderService.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class RecoverPaymentBuilderService implements PaymentBuilderService {
+ private final ScheduledChargesService scheduledChargesService;
+
+ @Autowired
+ public RecoverPaymentBuilderService(final ScheduledChargesService scheduledChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
+ }
+
+ @Override
+ public PaymentBuilder getPaymentBuilder(
+ final @Nonnull DataContextOfAction dataContextOfAction,
+ final @Nullable BigDecimal requestedDisbursalSize,
+ final LocalDate forDate,
+ final RunningBalances runningBalances) {
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+ final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.RECOVER, forDate));
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
+ productIdentifier, scheduledActions);
+
+ final BigDecimal loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize();
+
+ return CostComponentService.getCostComponentsForScheduledCharges(
+ scheduledCharges,
+ caseParameters.getBalanceRangeMaximum(),
+ runningBalances,
+ loanPaymentSize,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java
new file mode 100644
index 0000000..31f325f
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.IndividualLendingPatternFactory;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.api.v1.domain.Pattern;
+import io.mifos.portfolio.api.v1.domain.RequiredAccountAssignment;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @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_PRINCIPAL, negative);
+ this.put(AccountDesignators.CUSTOMER_LOAN_FEES, negative);
+ this.put(AccountDesignators.CUSTOMER_LOAN_INTEREST, 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.PRODUCT_LOSS_ALLOWANCE, negative);
+ this.put(AccountDesignators.GENERAL_LOSS_ALLOWANCE, negative);
+ this.put(AccountDesignators.GENERAL_EXPENSE, negative);
+ this.put(AccountDesignators.ENTRY, positive);
+ //TODO: derive signs from IndividualLendingPatternFactory.individualLendingRequiredAccounts instead.
+ }};
+
+ BigDecimal getAccountBalance(final String accountDesignator);
+
+ BigDecimal getAccruedBalanceForCharge(
+ final ChargeDefinition chargeDefinition);
+
+ Optional<LocalDateTime> getStartOfTerm(final DataContextOfAction dataContextOfAction);
+
+ default LocalDateTime getStartOfTermOrThrow(final DataContextOfAction dataContextOfAction) {
+ return this.getStartOfTerm(dataContextOfAction)
+ .orElseThrow(() -> ServiceException.internalError(
+ "Start of term for loan ''{0}'' could not be acquired from accounting.",
+ dataContextOfAction.getCompoundIdentifer()));
+ }
+
+ default BigDecimal getLedgerBalance(final String ledgerDesignator) {
+ final Pattern individualLendingPattern = IndividualLendingPatternFactory.individualLendingPattern();
+ return individualLendingPattern.getAccountAssignmentsRequired().stream()
+ .filter(requiredAccountAssignment -> ledgerDesignator.equals(requiredAccountAssignment.getGroup()))
+ .map(RequiredAccountAssignment::getAccountDesignator)
+ .map(this::getAccountBalance)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+ default BigDecimal getBalance(final String designator) {
+ final Pattern individualLendingPattern = IndividualLendingPatternFactory.individualLendingPattern();
+ if (individualLendingPattern.getAccountAssignmentGroups().contains(designator))
+ return getLedgerBalance(designator);
+ else
+ return getAccountBalance(designator);
+ }
+
+ 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/costcomponent/SimulatedRunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/SimulatedRunningBalances.java
new file mode 100644
index 0000000..fcd921d
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/SimulatedRunningBalances.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+
+import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+public class SimulatedRunningBalances implements RunningBalances {
+ final private Map<String, BigDecimal> balances = new HashMap<>();
+ private final LocalDateTime startOfTerm;
+
+ public SimulatedRunningBalances() {
+ this.startOfTerm = LocalDateTime.now(Clock.systemUTC());
+ }
+
+ SimulatedRunningBalances(final LocalDateTime startOfTerm) {
+ this.startOfTerm = startOfTerm;
+ }
+
+ @Override
+ public BigDecimal getAccountBalance(final String accountDesignator) {
+ return balances.getOrDefault(accountDesignator, BigDecimal.ZERO);
+ }
+
+ @Override
+ public BigDecimal getAccruedBalanceForCharge(
+ final ChargeDefinition chargeDefinition) {
+ return balances.getOrDefault(chargeDefinition.getAccrualAccountDesignator(), BigDecimal.ZERO);
+ //This is not accurate for all cases, but good enough for the cases it's used in.
+ }
+
+ @Override
+ public Optional<LocalDateTime> getStartOfTerm(final DataContextOfAction dataContextOfAction) {
+ return Optional.ofNullable(startOfTerm);
+ }
+
+ 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/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderService.java
new file mode 100644
index 0000000..3dab27b
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderService.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class WriteOffPaymentBuilderService implements PaymentBuilderService {
+ private final ScheduledChargesService scheduledChargesService;
+
+ @Autowired
+ public WriteOffPaymentBuilderService(final ScheduledChargesService scheduledChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
+ }
+
+ @Override
+ public PaymentBuilder getPaymentBuilder(
+ final @Nonnull DataContextOfAction dataContextOfAction,
+ final @Nullable BigDecimal ignored,
+ final LocalDate forDate,
+ final RunningBalances runningBalances)
+ {
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+ final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.WRITE_OFF, forDate));
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
+ productIdentifier, scheduledActions);
+
+ final BigDecimal loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize();
+
+ return CostComponentService.getCostComponentsForScheduledCharges(
+ scheduledCharges,
+ caseParameters.getBalanceRangeMaximum(),
+ runningBalances,
+ loanPaymentSize,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ChargeRange.java b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ChargeRange.java
similarity index 90%
rename from service/src/main/java/io/mifos/individuallending/internal/service/ChargeRange.java
rename to service/src/main/java/io/mifos/individuallending/internal/service/schedule/ChargeRange.java
index cd293d5..2c8acf8 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ChargeRange.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ChargeRange.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
import java.math.BigDecimal;
import java.util.Objects;
@@ -22,7 +22,7 @@
/**
* @author Myrle Krantz
*/
-class ChargeRange {
+public class ChargeRange {
final private BigDecimal from;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
final private Optional<BigDecimal> to;
@@ -34,7 +34,7 @@
this.to = to;
}
- boolean amountIsWithinRange(BigDecimal amountProportionalTo) {
+ public boolean amountIsWithinRange(final BigDecimal amountProportionalTo) {
return to.map(bigDecimal -> from.compareTo(amountProportionalTo) <= 0 &&
bigDecimal.compareTo(amountProportionalTo) > 0)
.orElseGet(() -> from.compareTo(amountProportionalTo) <= 0);
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/schedule/LossProvisionChargesService.java b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/LossProvisionChargesService.java
new file mode 100644
index 0000000..7bb2eb7
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/LossProvisionChargesService.java
@@ -0,0 +1,102 @@
+/*
+ * 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.schedule;
+
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
+import io.mifos.individuallending.api.v1.domain.product.LossProvisionStep;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.LossProvisionStepService;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Optional;
+
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.PROVISION_FOR_LOSSES_ID;
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.PROVISION_FOR_LOSSES_NAME;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class LossProvisionChargesService {
+ private final LossProvisionStepService lossProvisionStepService;
+
+ @Autowired
+ public LossProvisionChargesService(
+ final LossProvisionStepService lossProvisionStepService) {
+ this.lossProvisionStepService = lossProvisionStepService;
+ }
+
+ public Optional<ScheduledCharge> getScheduledChargeForMarkLate(
+ final DataContextOfAction dataContextOfAction,
+ final LocalDate forDate,
+ final int daysLate)
+ {
+ return getScheduledLossProvisioningCharge(dataContextOfAction, forDate, daysLate, Action.MARK_LATE);
+ }
+
+
+ public Optional<ScheduledCharge> getScheduledChargeForDisbursement(
+ final DataContextOfAction dataContextOfAction,
+ final LocalDate forDate)
+ {
+ return getScheduledLossProvisioningCharge(dataContextOfAction, forDate, 0, Action.DISBURSE);
+ }
+
+ private Optional<ScheduledCharge> getScheduledLossProvisioningCharge(
+ final DataContextOfAction dataContextOfAction,
+ final LocalDate forDate,
+ final int daysLate, Action action) {
+ final Optional<ChargeDefinition> optionalChargeDefinition = percentProvision(dataContextOfAction, daysLate)
+ .map(percentProvision -> getLossProvisionCharge(percentProvision, action));
+
+ return optionalChargeDefinition.map(chargeDefinition -> {
+ final ScheduledAction scheduledAction = new ScheduledAction(action, forDate);
+ return new ScheduledCharge(scheduledAction, chargeDefinition, Optional.empty());
+ });
+ }
+
+ private Optional<BigDecimal> percentProvision(
+ final DataContextOfAction dataContextOfAction,
+ final int daysLate)
+ {
+ return lossProvisionStepService.findByProductIdAndDaysLate(dataContextOfAction.getProductEntity().getId(), daysLate)
+ .map(LossProvisionStep::getPercentProvision);
+ }
+
+ private ChargeDefinition getLossProvisionCharge(
+ final BigDecimal percentProvision,
+ final Action action) {
+ final ChargeDefinition ret = new ChargeDefinition();
+ ret.setChargeAction(action.name());
+ ret.setIdentifier(PROVISION_FOR_LOSSES_ID);
+ ret.setName(PROVISION_FOR_LOSSES_NAME);
+ ret.setDescription(PROVISION_FOR_LOSSES_NAME);
+ ret.setFromAccountDesignator(AccountDesignators.PRODUCT_LOSS_ALLOWANCE);
+ ret.setAccrualAccountDesignator(AccountDesignators.GENERAL_LOSS_ALLOWANCE);
+ ret.setToAccountDesignator(AccountDesignators.GENERAL_EXPENSE);
+ ret.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_DESIGNATOR.getValue());
+ ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ ret.setAmount(percentProvision);
+ ret.setReadOnly(true);
+ return ret;
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/Period.java b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/Period.java
similarity index 82%
rename from service/src/main/java/io/mifos/individuallending/internal/service/Period.java
rename to service/src/main/java/io/mifos/individuallending/internal/service/schedule/Period.java
index 0ca16cd..db0e069 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/Period.java
@@ -13,9 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
-
-import io.mifos.core.lang.DateConverter;
+package io.mifos.individuallending.internal.service.schedule;
import javax.annotation.Nonnull;
import java.time.Duration;
@@ -31,25 +29,25 @@
final private LocalDate endDate;
final private boolean lastPeriod;
- Period(final LocalDate beginDate, final LocalDate endDateExclusive) {
+ public Period(final LocalDate beginDate, final LocalDate endDateExclusive) {
this.beginDate = beginDate;
this.endDate = endDateExclusive;
this.lastPeriod = false;
}
- Period(final LocalDate beginDate, final LocalDate endDateExclusive, final boolean lastPeriod) {
+ public Period(final LocalDate beginDate, final LocalDate endDateExclusive, final boolean lastPeriod) {
this.beginDate = beginDate;
this.endDate = endDateExclusive;
this.lastPeriod = lastPeriod;
}
- Period(final LocalDate beginDate, final int periodLength) {
+ public Period(final LocalDate beginDate, final int periodLength) {
this.beginDate = beginDate;
this.endDate = beginDate.plusDays(periodLength);
this.lastPeriod = false;
}
- Period(final int periodLength, final LocalDate endDate) {
+ public Period(final int periodLength, final LocalDate endDate) {
this.beginDate = endDate.minusDays(periodLength);
this.endDate = endDate;
this.lastPeriod = false;
@@ -59,19 +57,15 @@
return beginDate;
}
- LocalDate getEndDate() {
+ public LocalDate getEndDate() {
return endDate;
}
- boolean isLastPeriod() {
+ public boolean isLastPeriod() {
return lastPeriod;
}
- String getEndDateAsString() {
- return endDate == null ? null : DateConverter.toIsoString(endDate);
- }
-
- Duration getDuration() {
+ public Duration getDuration() {
long days = beginDate.until(endDate, ChronoUnit.DAYS);
return ChronoUnit.DAYS.getDuration().multipliedBy(days);
}
@@ -80,10 +74,6 @@
return this.getBeginDate().compareTo(date) <= 0 && this.getEndDate().compareTo(date) > 0;
}
- boolean isDefined() {
- return beginDate != null || endDate != null;
- }
-
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledAction.java
similarity index 69%
rename from service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java
rename to service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledAction.java
index edcef2e..098877e 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledAction.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
@@ -26,32 +26,35 @@
* @author Myrle Krantz
*/
public class ScheduledAction {
- final Action action;
- final LocalDate when;
- final @Nullable Period actionPeriod;
- final @Nullable Period repaymentPeriod;
+ private final Action action;
+ private final LocalDate when;
+ private final @Nullable Period actionPeriod;
+ private final @Nullable Period repaymentPeriod;
- ScheduledAction(@Nonnull final Action action,
- @Nonnull final LocalDate when,
- @Nonnull final Period actionPeriod,
- @Nonnull final Period repaymentPeriod) {
+ public ScheduledAction(
+ @Nonnull final Action action,
+ @Nonnull final LocalDate when,
+ @Nonnull final Period actionPeriod,
+ @Nonnull final Period repaymentPeriod) {
this.action = action;
this.when = when;
this.actionPeriod = actionPeriod;
this.repaymentPeriod = repaymentPeriod;
}
- ScheduledAction(@Nonnull final Action action,
- @Nonnull final LocalDate when,
- @Nonnull final Period actionPeriod) {
+ public ScheduledAction(
+ @Nonnull final Action action,
+ @Nonnull final LocalDate when,
+ @Nonnull final Period actionPeriod) {
this.action = action;
this.when = when;
this.actionPeriod = actionPeriod;
this.repaymentPeriod = null;
}
- ScheduledAction(@Nonnull final Action action,
- @Nonnull final LocalDate when) {
+ public ScheduledAction(
+ @Nonnull final Action action,
+ @Nonnull final LocalDate when) {
this.action = action;
this.when = when;
this.actionPeriod = null;
@@ -87,4 +90,22 @@
", repaymentPeriod=" + repaymentPeriod +
'}';
}
+
+ @Nullable
+ public Period getActionPeriod() {
+ return actionPeriod;
+ }
+
+ public Action getAction() {
+ return action;
+ }
+
+ @Nullable
+ public Period getRepaymentPeriod() {
+ return repaymentPeriod;
+ }
+
+ public LocalDate getWhen() {
+ return when;
+ }
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledActionHelpers.java
similarity index 98%
rename from service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
rename to service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledActionHelpers.java
index c6f4fc8..1807136 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledActionHelpers.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
@@ -61,7 +61,7 @@
final LocalDate effectiveEndOfTerm = fromDate.isAfter(endOfTerm) ? fromDate : endOfTerm;
return getHypotheticalScheduledActionsForDisbursedLoan(startOfTerm, effectiveEndOfTerm, caseParameters)
- .filter(x -> x.action.equals(Action.ACCEPT_PAYMENT))
+ .filter(x -> x.getAction().equals(Action.ACCEPT_PAYMENT))
.filter(x -> x.actionIsOnOrAfter(fromDate))
.findFirst()
.orElseGet(() -> new ScheduledAction(Action.ACCEPT_PAYMENT, fromDate));
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledCharge.java
similarity index 87%
rename from service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java
rename to service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledCharge.java
index 7b98317..8cfaf43 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledCharge.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
@@ -29,7 +29,7 @@
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private final Optional<ChargeRange> chargeRange;
- ScheduledCharge(
+ public ScheduledCharge(
@Nonnull final ScheduledAction scheduledAction,
@Nonnull final ChargeDefinition chargeDefinition,
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Nonnull final Optional<ChargeRange> chargeRange) {
@@ -38,15 +38,15 @@
this.chargeRange = chargeRange;
}
- ScheduledAction getScheduledAction() {
+ public ScheduledAction getScheduledAction() {
return scheduledAction;
}
- ChargeDefinition getChargeDefinition() {
+ public ChargeDefinition getChargeDefinition() {
return chargeDefinition;
}
- Optional<ChargeRange> getChargeRange() {
+ public Optional<ChargeRange> getChargeRange() {
return chargeRange;
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargeComparator.java b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargeComparator.java
similarity index 86%
rename from service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargeComparator.java
rename to service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargeComparator.java
index e597b48..9478c28 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargeComparator.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargeComparator.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
@@ -25,7 +25,7 @@
/**
* @author Myrle Krantz
*/
-class ScheduledChargeComparator implements Comparator<ScheduledCharge>
+public class ScheduledChargeComparator implements Comparator<ScheduledCharge>
{
@Override
public int compare(ScheduledCharge o1, ScheduledCharge o2) {
@@ -33,11 +33,11 @@
}
static int compareScheduledCharges(ScheduledCharge o1, ScheduledCharge o2) {
- int ret = o1.getScheduledAction().when.compareTo(o2.getScheduledAction().when);
+ int ret = o1.getScheduledAction().getWhen().compareTo(o2.getScheduledAction().getWhen());
if (ret != 0)
return ret;
- ret = o1.getScheduledAction().action.compareTo(o2.getScheduledAction().action);
+ ret = o1.getScheduledAction().getAction().compareTo(o2.getScheduledAction().getAction());
if (ret != 0)
return ret;
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargesService.java b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargesService.java
similarity index 95%
rename from service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargesService.java
rename to service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargesService.java
index b578443..bd18737 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargesService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargesService.java
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
+import io.mifos.individuallending.internal.service.ChargeDefinitionService;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.service.internal.repository.BalanceSegmentEntity;
import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
-import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -47,7 +47,7 @@
this.balanceSegmentRepository = balanceSegmentRepository;
}
- List<ScheduledCharge> getScheduledCharges(
+ public List<ScheduledCharge> getScheduledCharges(
final String productIdentifier,
final @Nonnull List<ScheduledAction> scheduledActions) {
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction
@@ -147,13 +147,13 @@
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
final ScheduledAction scheduledAction) {
final List<ChargeDefinition> chargeMappingList = chargeDefinitionsMappedByChargeAction
- .get(scheduledAction.action.name());
+ .get(scheduledAction.getAction().name());
Stream<ChargeDefinition> chargeMapping = chargeMappingList == null ? Stream.empty() : chargeMappingList.stream();
if (chargeMapping == null)
chargeMapping = Stream.empty();
final List<ChargeDefinition> accrueMappingList = chargeDefinitionsMappedByAccrueAction
- .get(scheduledAction.action.name());
+ .get(scheduledAction.getAction().name());
Stream<ChargeDefinition> accrueMapping = accrueMappingList == null ? Stream.empty() : accrueMappingList.stream();
if (accrueMapping == null)
accrueMapping = Stream.empty();
diff --git a/service/src/main/java/io/mifos/individuallending/rest/LossProvisionStepRestController.java b/service/src/main/java/io/mifos/individuallending/rest/LossProvisionStepRestController.java
new file mode 100644
index 0000000..24dc64b
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/rest/LossProvisionStepRestController.java
@@ -0,0 +1,91 @@
+/*
+ * 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.rest;
+
+import io.mifos.anubis.annotation.AcceptedTokenType;
+import io.mifos.anubis.annotation.Permittable;
+import io.mifos.core.command.gateway.CommandGateway;
+import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.api.v1.domain.product.LossProvisionConfiguration;
+import io.mifos.individuallending.internal.command.ChangeLossProvisionSteps;
+import io.mifos.individuallending.internal.service.LossProvisionStepService;
+import io.mifos.portfolio.api.v1.PermittableGroupIds;
+import io.mifos.portfolio.service.internal.service.ProductService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+
+/**
+ * @author Myrle Krantz
+ */
+@RestController
+@RequestMapping("/individuallending/products/{productidentifier}/lossprovisionconfiguration")
+public class LossProvisionStepRestController {
+ private final CommandGateway commandGateway;
+ private final ProductService productService;
+ private final LossProvisionStepService lossProvisionStepService;
+
+ @Autowired
+ public LossProvisionStepRestController(
+ final CommandGateway commandGateway,
+ final ProductService productService,
+ final LossProvisionStepService lossProvisionStepService) {
+ this.commandGateway = commandGateway;
+ this.productService = productService;
+ this.lossProvisionStepService = lossProvisionStepService;
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_LOSS_PROVISIONING_MANAGEMENT)
+ @RequestMapping(
+ method = RequestMethod.PUT,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public @ResponseBody
+ ResponseEntity<Void>
+ changeLossProvisionConfiguration(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @RequestBody @Valid LossProvisionConfiguration lossProvisionConfiguration) {
+ checkProductExists(productIdentifier);
+
+ commandGateway.process(new ChangeLossProvisionSteps(productIdentifier, lossProvisionConfiguration));
+
+ return new ResponseEntity<>(HttpStatus.ACCEPTED);
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_LOSS_PROVISIONING_MANAGEMENT)
+ @RequestMapping(
+ method = RequestMethod.GET,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public @ResponseBody
+ LossProvisionConfiguration
+ getLossProvisionConfiguration(
+ @PathVariable("productidentifier") final String productIdentifier) {
+ checkProductExists(productIdentifier);
+
+ return new LossProvisionConfiguration(lossProvisionStepService.findByProductIdentifier(productIdentifier));
+ }
+
+ private void checkProductExists(@PathVariable("productidentifier") String productIdentifier) {
+ productService.findByIdentifier(productIdentifier)
+ .orElseThrow(() -> ServiceException.notFound("Product not found ''{0}''.", productIdentifier));
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/checker/CaseChecker.java b/service/src/main/java/io/mifos/portfolio/service/internal/checker/CaseChecker.java
index 11b422f..e7b5510 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/checker/CaseChecker.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/checker/CaseChecker.java
@@ -29,7 +29,6 @@
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
-import java.util.Optional;
/**
* @author Myrle Krantz
@@ -56,16 +55,30 @@
caseService.findByIdentifier(productIdentifier, instance.getIdentifier())
.ifPresent(x -> {throw ServiceException.conflict("Duplicate identifier: " + productIdentifier + "." + x.getIdentifier());});
- final Optional<Boolean> productEnabled = productService.findEnabledByIdentifier(productIdentifier);
- if (!productEnabled.orElseThrow(() -> ServiceException.internalError("Product should exist, but doesn't"))) {
+ final Product product = productService.findByIdentifier(productIdentifier)
+ .orElseThrow(() -> ServiceException.badRequest("Product must exist ''{0}''.", productIdentifier));
+ final Boolean productEnabled = product.isEnabled();
+ if (!productEnabled) {
throw ServiceException.badRequest("Product must be enabled before cases for it can be created: " + productIdentifier);}
- checkForChange(productIdentifier, instance);
+ validateParameters(productIdentifier, instance, product);
}
public void checkForChange(final String productIdentifier, final Case instance) {
final Product product = productService.findByIdentifier(productIdentifier)
.orElseThrow(() -> ServiceException.badRequest("Product must exist ''{0}''.", productIdentifier));
+
+ final Case.State currentState = Case.State.valueOf(instance.getCurrentState());
+ if (currentState.equals(Case.State.ACTIVE) || currentState.equals(Case.State.CLOSED) || currentState.equals(Case.State.APPROVED))
+ throw ServiceException.badRequest("You may not change a case after it has been approved or closed.");
+
+ validateParameters(productIdentifier, instance, product);
+ }
+
+ private void validateParameters(
+ final String productIdentifier,
+ final Case instance,
+ final Product product) {
final InterestRange interestRange = product.getInterestRange();
final BigDecimal interest = instance.getInterest();
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ProductCommandHandler.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ProductCommandHandler.java
index c075416..6a284a6 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ProductCommandHandler.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ProductCommandHandler.java
@@ -37,9 +37,8 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
-import java.util.List;
import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* @author Myrle Krantz
@@ -78,7 +77,7 @@
final ProductEntity productEntity = ProductMapper.map(createProductCommand.getInstance(), false);
this.productRepository.save(productEntity);
- patternFactory.charges().forEach(charge -> createChargeDefinition(productEntity, charge));
+ patternFactory.defaultConfigurableCharges().forEach(charge -> createChargeDefinition(productEntity, charge));
return createProductCommand.getInstance().getIdentifier();
}
@@ -139,11 +138,9 @@
//noinspection PointlessBooleanExpression
if (changeEnablingOfProductCommand.getEnabled() == true) {
final Set<AccountAssignment> accountAssignments = ProductMapper.map(productEntity).getAccountAssignments();
- final List<ChargeDefinition> chargeDefinitions = chargeDefinitionRepository
+ final Stream<ChargeDefinition> chargeDefinitions = chargeDefinitionRepository
.findByProductId(productEntity.getIdentifier())
- .stream()
- .map(ChargeDefinitionMapper::map)
- .collect(Collectors.toList());
+ .map(ChargeDefinitionMapper::map);
final Set<String> accountAssignmentsRequiredButNotProvided
= AccountingAdapter.accountAssignmentsRequiredButNotProvided(accountAssignments, chargeDefinitions);
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..fba4800 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,15 +100,11 @@
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:
+ case REPAY_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/repository/CaseCommandEntity.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseCommandEntity.java
new file mode 100644
index 0000000..062726e
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseCommandEntity.java
@@ -0,0 +1,114 @@
+/*
+ * 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.portfolio.service.internal.repository;
+
+import io.mifos.core.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Entity
+@Table(name = "bastet_case_commands")
+public class CaseCommandEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @Column(name = "case_id")
+ private Long caseId;
+
+ @Column(name = "action_name")
+ private String actionName;
+
+ @Column(name = "created_on")
+ @Convert(converter = LocalDateTimeConverter.class)
+ private LocalDateTime createdOn;
+
+ @Column(name = "created_by")
+ private String createdBy;
+
+ @Column(name = "thoth_transaction_uq")
+ private String transactionUniqueifier;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getCaseId() {
+ return caseId;
+ }
+
+ public void setCaseId(Long caseId) {
+ this.caseId = caseId;
+ }
+
+ public String getActionName() {
+ return actionName;
+ }
+
+ public void setActionName(String actionName) {
+ this.actionName = actionName;
+ }
+
+ public LocalDateTime getCreatedOn() {
+ return createdOn;
+ }
+
+ public void setCreatedOn(LocalDateTime createdOn) {
+ this.createdOn = createdOn;
+ }
+
+ public String getCreatedBy() {
+ return createdBy;
+ }
+
+ public void setCreatedBy(String createdBy) {
+ this.createdBy = createdBy;
+ }
+
+ public String getTransactionUniqueifier() {
+ return transactionUniqueifier;
+ }
+
+ public void setTransactionUniqueifier(String transactionUniqueifier) {
+ this.transactionUniqueifier = transactionUniqueifier;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CaseCommandEntity that = (CaseCommandEntity) o;
+ return Objects.equals(caseId, that.caseId) &&
+ Objects.equals(actionName, that.actionName) &&
+ Objects.equals(transactionUniqueifier, that.transactionUniqueifier);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(caseId, actionName, transactionUniqueifier);
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseCommandRepository.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseCommandRepository.java
new file mode 100644
index 0000000..fa78532
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/CaseCommandRepository.java
@@ -0,0 +1,29 @@
+/*
+ * 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.portfolio.service.internal.repository;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author Myrle Krantz
+ */
+@Repository
+public interface CaseCommandRepository extends JpaRepository<CaseCommandEntity, Long> {
+ Page<CaseCommandEntity> findByCaseIdAndActionName(Long caseId, String actionName, Pageable pageable);
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionRepository.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionRepository.java
index 8ff39e9..ededa8b 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionRepository.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionRepository.java
@@ -20,8 +20,8 @@
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
-import java.util.List;
import java.util.Optional;
+import java.util.stream.Stream;
/**
* @author Myrle Krantz
@@ -30,7 +30,7 @@
public interface ChargeDefinitionRepository extends JpaRepository<ChargeDefinitionEntity, Long> {
@SuppressWarnings("JpaQlInspection")
@Query("SELECT t FROM ChargeDefinitionEntity t WHERE t.product.identifier = :productIdentifier")
- List<ChargeDefinitionEntity> findByProductId(@Param("productIdentifier") String productId);
+ Stream<ChargeDefinitionEntity> findByProductId(@Param("productIdentifier") String productId);
@SuppressWarnings("JpaQlInspection")
@Query("SELECT t FROM ChargeDefinitionEntity t WHERE t.product.identifier = :productIdentifier AND t.identifier = :chargeDefinitionIdentifier")
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/service/ChargeDefinitionService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/ChargeDefinitionService.java
deleted file mode 100644
index 0230368..0000000
--- a/service/src/main/java/io/mifos/portfolio/service/internal/service/ChargeDefinitionService.java
+++ /dev/null
@@ -1,76 +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.portfolio.service.internal.service;
-
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
-import io.mifos.portfolio.service.internal.mapper.ChargeDefinitionMapper;
-import io.mifos.portfolio.service.internal.repository.ChargeDefinitionRepository;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.Nonnull;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-/**
- * @author Myrle Krantz
- */
-@Service
-public class ChargeDefinitionService {
- private final ChargeDefinitionRepository chargeDefinitionRepository;
-
- @Autowired
- public ChargeDefinitionService(final ChargeDefinitionRepository chargeDefinitionRepository) {
- this.chargeDefinitionRepository = chargeDefinitionRepository;
- }
-
- public List<ChargeDefinition> findAllEntities(final String productIdentifier) {
- return chargeDefinitionRepository.findByProductId(productIdentifier).stream()
- .map(ChargeDefinitionMapper::map)
- .collect(Collectors.toList());
- }
-
- @Nonnull
- public Map<String, List<ChargeDefinition>> getChargeDefinitionsMappedByChargeAction(
- final String productIdentifier)
- {
- final List<ChargeDefinition> chargeDefinitions = findAllEntities(productIdentifier);
-
- return chargeDefinitions.stream()
- .collect(Collectors.groupingBy(ChargeDefinition::getChargeAction,
- Collectors.mapping(x -> x, Collectors.toList())));
- }
-
- @Nonnull
- public Map<String, List<ChargeDefinition>> getChargeDefinitionsMappedByAccrueAction(
- final String productIdentifier)
- {
- final List<ChargeDefinition> chargeDefinitions = findAllEntities(productIdentifier);
-
- return chargeDefinitions.stream()
- .filter(x -> x.getAccrueAction() != null)
- .collect(Collectors.groupingBy(ChargeDefinition::getAccrueAction,
- Collectors.mapping(x -> x, Collectors.toList())));
- }
-
- public Optional<ChargeDefinition> findByIdentifier(final String productIdentifier, final String identifier) {
- return chargeDefinitionRepository
- .findByProductIdAndChargeDefinitionIdentifier(productIdentifier, identifier)
- .map(ChargeDefinitionMapper::map);
- }
-}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/service/ConfigurableChargeDefinitionService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/ConfigurableChargeDefinitionService.java
new file mode 100644
index 0000000..902e06d
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/service/ConfigurableChargeDefinitionService.java
@@ -0,0 +1,49 @@
+/*
+ * 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.service.internal.service;
+
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.service.internal.mapper.ChargeDefinitionMapper;
+import io.mifos.portfolio.service.internal.repository.ChargeDefinitionRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class ConfigurableChargeDefinitionService {
+ private final ChargeDefinitionRepository chargeDefinitionRepository;
+
+ @Autowired
+ public ConfigurableChargeDefinitionService(final ChargeDefinitionRepository chargeDefinitionRepository) {
+ this.chargeDefinitionRepository = chargeDefinitionRepository;
+ }
+
+ public Stream<ChargeDefinition> findAllEntities(final String productIdentifier) {
+ return chargeDefinitionRepository.findByProductId(productIdentifier)
+ .map(ChargeDefinitionMapper::map);
+ }
+
+ public Optional<ChargeDefinition> findByIdentifier(final String productIdentifier, final String identifier) {
+ return chargeDefinitionRepository
+ .findByProductIdAndChargeDefinitionIdentifier(productIdentifier, identifier)
+ .map(ChargeDefinitionMapper::map);
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/service/PatternService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/PatternService.java
index 2d40f29..457d529 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/service/PatternService.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/service/PatternService.java
@@ -15,11 +15,9 @@
*/
package io.mifos.portfolio.service.internal.service;
-import io.mifos.core.lang.ServiceException;
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.Pattern;
-import io.mifos.products.spi.PatternFactory;
import io.mifos.portfolio.service.internal.pattern.PatternFactoryRegistry;
+import io.mifos.products.spi.PatternFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -50,10 +48,4 @@
{
return patternFactoryRegistry.getPatternFactoryForPackage(identifier).map(PatternFactory::pattern);
}
-
- public List<ChargeDefinition> findDefaultChargeDefinitions(final String patternPackage) {
- return patternFactoryRegistry.getPatternFactoryForPackage(patternPackage)
- .orElseThrow(() -> ServiceException.notFound("Pattern with package " + patternPackage + " doesn't exist."))
- .charges();
- }
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java
index 1bc10d4..4942da1 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/service/ProductService.java
@@ -34,6 +34,7 @@
import javax.annotation.Nullable;
import java.util.*;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* @author Myrle Krantz
@@ -42,16 +43,16 @@
public class ProductService {
private final ProductRepository productRepository;
- private final ChargeDefinitionService chargeDefinitionService;
+ private final ConfigurableChargeDefinitionService configurableChargeDefinitionService;
private final AccountingAdapter accountingAdapter;
@Autowired
public ProductService(final ProductRepository productRepository,
- final ChargeDefinitionService chargeDefinitionService,
+ final ConfigurableChargeDefinitionService configurableChargeDefinitionService,
final AccountingAdapter accountingAdapter) {
super();
this.productRepository = productRepository;
- this.chargeDefinitionService = chargeDefinitionService;
+ this.configurableChargeDefinitionService = configurableChargeDefinitionService;
this.accountingAdapter = accountingAdapter;
}
@@ -120,12 +121,12 @@
return false;
final Product product = maybeProduct.get();
final Set<AccountAssignment> accountAssignments = product.getAccountAssignments();
- final List<ChargeDefinition> chargeDefinitions = chargeDefinitionService.findAllEntities(identifier);
+ final Stream<ChargeDefinition> chargeDefinitions = configurableChargeDefinitionService.findAllEntities(identifier);
return AccountingAdapter.accountAssignmentsRequiredButNotProvided(accountAssignments, chargeDefinitions).isEmpty();
}
public Set<AccountAssignment> getIncompleteAccountAssignments(final String identifier) {
- final Set<String> requiredAccountDesignators = AccountingAdapter.getRequiredAccountDesignators(chargeDefinitionService.findAllEntities(identifier));
+ final Set<String> requiredAccountDesignators = AccountingAdapter.getRequiredAccountDesignators(configurableChargeDefinitionService.findAllEntities(identifier));
final AccountAssignmentValidator accountAssignmentValidator
= new AccountAssignmentValidator(findByIdentifier(identifier)
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..4bb8623 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
@@ -15,19 +15,19 @@
*/
package io.mifos.portfolio.service.internal.util;
-import io.mifos.accounting.api.v1.client.AccountAlreadyExistsException;
-import io.mifos.accounting.api.v1.client.AccountNotFoundException;
-import io.mifos.accounting.api.v1.client.LedgerManager;
-import io.mifos.accounting.api.v1.client.LedgerNotFoundException;
+import io.mifos.accounting.api.v1.client.*;
import io.mifos.accounting.api.v1.domain.*;
import io.mifos.core.api.util.UserContextHolder;
import io.mifos.core.lang.DateConverter;
import io.mifos.core.lang.DateRange;
import io.mifos.core.lang.ServiceException;
+import io.mifos.core.lang.listening.EventExpectation;
+import io.mifos.individuallending.internal.service.DesignatorToAccountIdentifierMapper;
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.service.ServiceConstants;
import org.apache.commons.lang.RandomStringUtils;
+import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -38,6 +38,7 @@
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -49,53 +50,122 @@
@Component
public class AccountingAdapter {
+
public enum IdentifierType {LEDGER, ACCOUNT}
private final LedgerManager ledgerManager;
+ private final AccountingListener accountingListener;
private final Logger logger;
@Autowired
public AccountingAdapter(@SuppressWarnings("SpringJavaAutowiringInspection") final LedgerManager ledgerManager,
+ final AccountingListener accountingListener,
@Qualifier(ServiceConstants.LOGGER_NAME) final Logger logger) {
this.ledgerManager = ledgerManager;
+ this.accountingListener = accountingListener;
this.logger = logger;
}
- public void bookCharges(final List<ChargeInstance> costComponents,
- final String note,
- final String transactionDate,
- final String message,
- final String transactionType) {
- final Set<Creditor> creditors = costComponents.stream()
- .map(AccountingAdapter::mapToCreditor)
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toSet());
- final Set<Debtor> debtors = costComponents.stream()
- .map(AccountingAdapter::mapToDebtor)
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toSet());
+ private static class BalanceAdjustment {
+ final private String accountIdentifier; //*Not* designator.
+ final private BigDecimal adjustment;
+
+ BalanceAdjustment(String accountIdentifier, BigDecimal adjustment) {
+ this.accountIdentifier = accountIdentifier;
+ this.adjustment = adjustment;
+ }
+
+ String getAccountIdentifier() {
+ return accountIdentifier;
+ }
+
+ BigDecimal getAdjustment() {
+ return adjustment;
+ }
+ }
+
+ public Optional<String> bookCharges(
+ final Map<String, BigDecimal> balanceAdjustments,
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper,
+ final String note,
+ final String transactionDate,
+ final String message,
+ final String transactionType) {
+ final String transactionUniqueifier = RandomStringUtils.random(26, true, true);
+ final JournalEntry journalEntry = getJournalEntry(
+ balanceAdjustments,
+ designatorToAccountIdentifierMapper,
+ note,
+ transactionDate,
+ message,
+ transactionType,
+ transactionUniqueifier,
+ UserContextHolder.checkedGetUser());
+
+ //noinspection ConstantConditions
+ if (journalEntry.getCreditors().isEmpty() && journalEntry.getDebtors().isEmpty())
+ return Optional.empty();
+
+ ledgerManager.createJournalEntry(journalEntry);
+ return Optional.of(transactionUniqueifier);
+ }
+
+ static JournalEntry getJournalEntry(
+ final Map<String, BigDecimal> balanceAdjustments,
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper,
+ final String note,
+ final String transactionDate,
+ final String message,
+ final String transactionType,
+ final String transactionUniqueifier,
+ final String user) {
+ final JournalEntry journalEntry = new JournalEntry();
+ final Set<Creditor> creditors = new HashSet<>();
+ journalEntry.setCreditors(creditors);
+ final Set<Debtor> debtors = new HashSet<>();
+ journalEntry.setDebtors(debtors);
+ final Map<String, BigDecimal> summedBalanceAdjustments = balanceAdjustments.entrySet().stream()
+ .map(entry -> {
+ final String accountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(entry.getKey());
+ return new BalanceAdjustment(accountIdentifier, entry.getValue());
+ })
+ .collect(Collectors.groupingBy(BalanceAdjustment::getAccountIdentifier,
+ Collectors.mapping(BalanceAdjustment::getAdjustment,
+ Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));
+
+ summedBalanceAdjustments.forEach((accountIdentifier, balanceAdjustment) -> {
+ final int sign = balanceAdjustment.compareTo(BigDecimal.ZERO);
+ if (sign == 0)
+ return;
+
+ if (sign < 0) {
+ final Debtor debtor = new Debtor();
+ debtor.setAccountNumber(accountIdentifier);
+ debtor.setAmount(balanceAdjustment.negate().toPlainString());
+ debtors.add(debtor);
+ } else {
+ final Creditor creditor = new Creditor();
+ creditor.setAccountNumber(accountIdentifier);
+ creditor.setAmount(balanceAdjustment.toPlainString());
+ creditors.add(creditor);
+ }
+ });
if (creditors.isEmpty() && !debtors.isEmpty() ||
debtors.isEmpty() && !creditors.isEmpty())
throw ServiceException.internalError("either only creditors or only debtors were provided.");
- //noinspection ConstantConditions
- if (creditors.isEmpty() && debtors.isEmpty())
- return;
- final JournalEntry journalEntry = new JournalEntry();
+ final String transactionIdentifier = "portfolio." + message + "." + transactionUniqueifier;
journalEntry.setCreditors(creditors);
journalEntry.setDebtors(debtors);
- journalEntry.setClerk(UserContextHolder.checkedGetUser());
+ journalEntry.setClerk(user);
journalEntry.setTransactionDate(transactionDate);
journalEntry.setMessage(message);
journalEntry.setTransactionType(transactionType);
journalEntry.setNote(note);
- journalEntry.setTransactionIdentifier("portfolio." + message + "." + RandomStringUtils.random(26, true, true));
-
- ledgerManager.createJournalEntry(journalEntry);
+ journalEntry.setTransactionIdentifier(transactionIdentifier);
+ return journalEntry;
}
public Optional<LocalDateTime> getDateOfOldestEntryContainingMessage(final String accountIdentifier,
@@ -133,36 +203,56 @@
.map(BigDecimal::valueOf).reduce(BigDecimal.ZERO, BigDecimal::add);
}
- private static Optional<Debtor> mapToDebtor(final ChargeInstance chargeInstance) {
- if (chargeInstance.getAmount().compareTo(BigDecimal.ZERO) == 0)
- return Optional.empty();
-
- final Debtor ret = new Debtor();
- ret.setAccountNumber(chargeInstance.getFromAccount());
- ret.setAmount(chargeInstance.getAmount().toPlainString());
- return Optional.of(ret);
- }
-
- private static Optional<Creditor> mapToCreditor(final ChargeInstance chargeInstance) {
- if (chargeInstance.getAmount().compareTo(BigDecimal.ZERO) == 0)
- return Optional.empty();
-
- final Creditor ret = new Creditor();
- ret.setAccountNumber(chargeInstance.getToAccount());
- ret.setAmount(chargeInstance.getAmount().toPlainString());
- return Optional.of(ret);
- }
-
- public BigDecimal getCurrentBalance(final String accountIdentifier) {
+ public BigDecimal getCurrentAccountBalance(final String accountIdentifier) {
try {
final Account account = ledgerManager.findAccount(accountIdentifier);
+ if (account == null || account.getBalance() == 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);
}
}
+ public String createLedger(
+ final String customerIdentifier,
+ final String groupName,
+ final String parentLedger) throws InterruptedException {
+ final Ledger ledger = ledgerManager.findLedger(parentLedger);
+ final List<Ledger> subLedgers = ledger.getSubLedgers() == null ? Collections.emptyList() : ledger.getSubLedgers();
+
+ final Ledger generatedLedger = new Ledger();
+ generatedLedger.setShowAccountsInChart(true);
+ generatedLedger.setParentLedgerIdentifier(parentLedger);
+ generatedLedger.setType(ledger.getType());
+ final IdentiferWithIndex ledgerIdentifer = createLedgerIdentifier(customerIdentifier, groupName, subLedgers);
+ generatedLedger.setIdentifier(ledgerIdentifer.getIdentifier());
+ generatedLedger.setDescription("Individual loan case specific ledger");
+ generatedLedger.setName(ledgerIdentifer.getIdentifier());
+
+
+ final EventExpectation expectation = accountingListener.expectLedgerCreation(generatedLedger.getIdentifier());
+ boolean created = false;
+ while (!created) {
+ try {
+ logger.info("Attempting to create ledger with identifier '{}'", ledgerIdentifer.getIdentifier());
+ ledgerManager.addSubLedger(parentLedger, generatedLedger);
+ created = true;
+ } catch (final LedgerAlreadyExistsException e) {
+ ledgerIdentifer.incrementIndex();
+ generatedLedger.setIdentifier(ledgerIdentifer.getIdentifier());
+ generatedLedger.setName(ledgerIdentifer.getIdentifier());
+ }
+ }
+ final boolean ledgerCreationDetected = expectation.waitForOccurrence(5, TimeUnit.SECONDS);
+ if (!ledgerCreationDetected)
+ logger.warn("Waited 5 seconds for creation of ledger '{}', but it was not detected. This could cause subsequent " +
+ "account creations to fail. Is there something wrong with the accounting service? Is ActiveMQ setup properly?",
+ generatedLedger.getIdentifier());
+ return ledgerIdentifer.getIdentifier();
+ }
+
public String createAccountForLedgerAssignment(final String customerIdentifier, final AccountAssignment ledgerAssignment) {
final Ledger ledger = ledgerManager.findLedger(ledgerAssignment.getLedgerIdentifier());
final AccountPage accountsOfLedger = ledgerManager.fetchAccountsOfLedger(ledger.getIdentifier(), null, null, null, null);
@@ -195,15 +285,49 @@
customerIdentifier, ledgerAssignment.getDesignator(), ledgerAssignment.getLedgerIdentifier()));
}
+ private static class IdentiferWithIndex {
+ private long index;
+ private final String prefix;
+
+ IdentiferWithIndex(long index, String prefix) {
+ this.index = index;
+ this.prefix = prefix;
+ }
+
+ String getIdentifier() {
+ return prefix + String.format("%05d", index);
+ }
+
+ void incrementIndex() {
+ index++;
+ }
+ }
+
+ private IdentiferWithIndex createLedgerIdentifier(
+ final String customerIdentifier,
+ final String groupName,
+ final List<Ledger> subLedgers) {
+ final String partialCustomerIdentifer = StringUtils.left(customerIdentifier, 22);
+ final String partialGroupName = StringUtils.left(groupName, 3);
+ final Set<String> subLedgerIdentifiers = subLedgers.stream().map(Ledger::getIdentifier).collect(Collectors.toSet());
+ final String generatedIdentifierPrefix = partialCustomerIdentifer + "." + partialGroupName + ".";
+ final IdentiferWithIndex ret = new IdentiferWithIndex(0, generatedIdentifierPrefix);
+ while (true) {
+ ret.incrementIndex();
+ if (!subLedgerIdentifiers.contains(ret.getIdentifier()))
+ return ret;
+ }
+ }
+
private String createAccountNumber(final String customerIdentifier, final String designator, final long accountIndex) {
- return customerIdentifier + "." + designator
+ return StringUtils.left(customerIdentifier, 22) + "." + StringUtils.left(designator, 3)
+ "." + String.format("%05d", accountIndex);
}
public static Set<String> accountAssignmentsRequiredButNotProvided(
final Set<AccountAssignment> accountAssignments,
- final List<ChargeDefinition> chargeDefinitionEntities) {
+ final Stream<ChargeDefinition> chargeDefinitionEntities) {
final Set<String> allAccountDesignatorsRequired = getRequiredAccountDesignators(chargeDefinitionEntities);
final Set<String> allAccountDesignatorsDefined = accountAssignments.stream().map(AccountAssignment::getDesignator)
.collect(Collectors.toSet());
@@ -215,8 +339,8 @@
}
}
- public static Set<String> getRequiredAccountDesignators(final Collection<ChargeDefinition> chargeDefinitionEntities) {
- return chargeDefinitionEntities.stream()
+ public static Set<String> getRequiredAccountDesignators(final Stream<ChargeDefinition> chargeDefinitionEntities) {
+ return chargeDefinitionEntities
.flatMap(AccountingAdapter::getAutomaticActionAccountDesignators)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingListener.java b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingListener.java
new file mode 100644
index 0000000..034510a
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingListener.java
@@ -0,0 +1,49 @@
+/*
+ * 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.portfolio.service.internal.util;
+
+import io.mifos.accounting.api.v1.EventConstants;
+import io.mifos.core.lang.TenantContextHolder;
+import io.mifos.core.lang.config.TenantHeaderFilter;
+import io.mifos.core.lang.listening.EventExpectation;
+import io.mifos.core.lang.listening.EventKey;
+import io.mifos.core.lang.listening.TenantedEventListener;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Myrle Krantz
+ */
+@Component
+public class AccountingListener {
+ private final TenantedEventListener eventListener = new TenantedEventListener();
+
+ @JmsListener(
+ destination = EventConstants.DESTINATION,
+ selector = EventConstants.SELECTOR_POST_LEDGER,
+ subscription = EventConstants.DESTINATION
+ )
+ public void onPostLedger(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+ final String payload) {
+ this.eventListener.notify(new EventKey(tenant, EventConstants.POST_LEDGER, payload));
+ }
+
+
+ EventExpectation expectLedgerCreation(final String ledgerIdentifier) {
+ return eventListener.expect(new EventKey(TenantContextHolder.checkedGetIdentifier(), EventConstants.POST_LEDGER, ledgerIdentifier));
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/util/ChargeInstance.java b/service/src/main/java/io/mifos/portfolio/service/internal/util/ChargeInstance.java
deleted file mode 100644
index 0056c84..0000000
--- a/service/src/main/java/io/mifos/portfolio/service/internal/util/ChargeInstance.java
+++ /dev/null
@@ -1,72 +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.portfolio.service.internal.util;
-
-import java.math.BigDecimal;
-import java.util.Objects;
-
-/**
- * @author Myrle Krantz
- */
-public class ChargeInstance {
- private final String fromAccount;
- private final String toAccount;
- private final BigDecimal amount;
-
- public ChargeInstance(final String fromAccount,
- final String toAccount,
- final BigDecimal amount) {
- this.fromAccount = fromAccount;
- this.toAccount = toAccount;
- this.amount = amount;
- }
-
- public String getFromAccount() {
- return fromAccount;
- }
-
- public String getToAccount() {
- return toAccount;
- }
-
- public BigDecimal getAmount() {
- return amount;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ChargeInstance that = (ChargeInstance) o;
- return Objects.equals(fromAccount, that.fromAccount) &&
- Objects.equals(toAccount, that.toAccount) &&
- Objects.equals(amount, that.amount);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(fromAccount, toAccount, amount);
- }
-
- @Override
- public String toString() {
- return "ChargeInstance{" +
- "fromAccount='" + fromAccount + '\'' +
- ", toAccount='" + toAccount + '\'' +
- ", amount=" + amount +
- '}';
- }
-}
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..086a9c8 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;
/**
@@ -167,7 +166,6 @@
this.commandGateway.process(new ChangeCaseCommand(instance));
return new ResponseEntity<>(HttpStatus.ACCEPTED);
- //TODO: Make sure case can't be changed from certain states.
}
@Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CASE_MANAGEMENT)
@@ -194,12 +192,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/portfolio/service/rest/ChargeDefinitionRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/ChargeDefinitionRestController.java
index df91bb3..d77dc4d 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/ChargeDefinitionRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/ChargeDefinitionRestController.java
@@ -23,7 +23,7 @@
import io.mifos.portfolio.service.internal.command.ChangeChargeDefinitionCommand;
import io.mifos.portfolio.service.internal.command.CreateChargeDefinitionCommand;
import io.mifos.portfolio.service.internal.command.DeleteProductChargeDefinitionCommand;
-import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
+import io.mifos.portfolio.service.internal.service.ConfigurableChargeDefinitionService;
import io.mifos.portfolio.service.internal.service.ProductService;
import io.mifos.core.command.gateway.CommandGateway;
import io.mifos.core.lang.ServiceException;
@@ -35,6 +35,7 @@
import javax.validation.Valid;
import java.util.List;
+import java.util.stream.Collectors;
/**
* @author Myrle Krantz
@@ -44,15 +45,15 @@
@RequestMapping("/products/{productidentifier}/charges/")
public class ChargeDefinitionRestController {
private final CommandGateway commandGateway;
- private final ChargeDefinitionService chargeDefinitionService;
+ private final ConfigurableChargeDefinitionService configurableChargeDefinitionService;
private final ProductService productService;
@Autowired
public ChargeDefinitionRestController(
- final CommandGateway commandGateway,
- final ChargeDefinitionService chargeDefinitionService, final ProductService productService) {
+ final CommandGateway commandGateway,
+ final ConfigurableChargeDefinitionService configurableChargeDefinitionService, final ProductService productService) {
this.commandGateway = commandGateway;
- this.chargeDefinitionService = chargeDefinitionService;
+ this.configurableChargeDefinitionService = configurableChargeDefinitionService;
this.productService = productService;
}
@@ -68,7 +69,8 @@
{
checkProductExists(productIdentifier);
- return chargeDefinitionService.findAllEntities(productIdentifier);
+ return configurableChargeDefinitionService.findAllEntities(productIdentifier)
+ .collect(Collectors.toList());
}
@Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
@@ -87,7 +89,7 @@
if (instance.isReadOnly())
throw ServiceException.badRequest("Created charges cannot be read only.");
- chargeDefinitionService.findByIdentifier(productIdentifier, instance.getIdentifier())
+ configurableChargeDefinitionService.findByIdentifier(productIdentifier, instance.getIdentifier())
.ifPresent(taskDefinition -> {throw ServiceException.conflict("Duplicate identifier: " + taskDefinition.getIdentifier());});
this.commandGateway.process(new CreateChargeDefinitionCommand(productIdentifier, instance));
@@ -107,7 +109,7 @@
{
checkProductExists(productIdentifier);
- return chargeDefinitionService.findByIdentifier(productIdentifier, chargeDefinitionIdentifier).orElseThrow(
+ return configurableChargeDefinitionService.findByIdentifier(productIdentifier, chargeDefinitionIdentifier).orElseThrow(
() -> ServiceException.notFound("No charge definition with the identifier '" + chargeDefinitionIdentifier + "' found."));
}
@@ -123,7 +125,7 @@
@PathVariable("chargedefinitionidentifier") final String chargeDefinitionIdentifier,
@RequestBody @Valid final ChargeDefinition instance)
{
- checkChargeExistsInProductAndIsNotReadOnly(productIdentifier, chargeDefinitionIdentifier);
+ checkChargeExistsInProduct(productIdentifier, chargeDefinitionIdentifier);
if (instance.isReadOnly())
throw ServiceException.badRequest("Created charges cannot be read only.");
@@ -147,23 +149,18 @@
@PathVariable("productidentifier") final String productIdentifier,
@PathVariable("chargedefinitionidentifier") final String chargeDefinitionIdentifier)
{
- checkChargeExistsInProductAndIsNotReadOnly(productIdentifier, chargeDefinitionIdentifier);
+ checkChargeExistsInProduct(productIdentifier, chargeDefinitionIdentifier);
commandGateway.process(new DeleteProductChargeDefinitionCommand(productIdentifier, chargeDefinitionIdentifier));
return ResponseEntity.accepted().build();
}
- private void checkChargeExistsInProductAndIsNotReadOnly(final String productIdentifier,
- final String chargeDefinitionIdentifier) {
- final boolean readOnly = chargeDefinitionService.findByIdentifier(productIdentifier, chargeDefinitionIdentifier)
+ private void checkChargeExistsInProduct(final String productIdentifier,
+ final String chargeDefinitionIdentifier) {
+ configurableChargeDefinitionService.findByIdentifier(productIdentifier, chargeDefinitionIdentifier)
.orElseThrow(() -> ServiceException.notFound("No charge definition ''{0}.{1}'' found.",
- productIdentifier, chargeDefinitionIdentifier))
- .isReadOnly();
-
- if (readOnly)
- throw ServiceException.conflict("Charge definition is read only ''{0}.{1}''",
- productIdentifier, chargeDefinitionIdentifier);
+ productIdentifier, chargeDefinitionIdentifier));
}
private void checkProductExists(final String productIdentifier) {
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/PatternRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/PatternRestController.java
index 408453b..cf10ec5 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/PatternRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/PatternRestController.java
@@ -17,14 +17,15 @@
import io.mifos.anubis.annotation.AcceptedTokenType;
import io.mifos.anubis.annotation.Permittable;
-import io.mifos.core.lang.ServiceException;
import io.mifos.portfolio.api.v1.PermittableGroupIds;
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.Pattern;
import io.mifos.portfolio.service.internal.service.PatternService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@@ -53,19 +54,4 @@
List<Pattern> getAllPatterns() {
return this.patternService.findAllEntities();
}
-
- @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
- @RequestMapping(
- value = "/{patternpackage}/charges/",
- method = RequestMethod.GET,
- consumes = MediaType.ALL_VALUE,
- produces = MediaType.APPLICATION_JSON_VALUE
- )
- public
- @ResponseBody
- List<ChargeDefinition> getAllDefaultChargeDefinitionsForPattern(@PathVariable("patternpackage") final String patternPackage) {
- final Pattern pattern = this.patternService.findByIdentifier(patternPackage)
- .orElseThrow(() -> ServiceException.notFound("Pattern with package " + patternPackage + " doesn't exist."));
- return this.patternService.findDefaultChargeDefinitions(patternPackage);
- }
}
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..6802e3f 100644
--- a/service/src/main/java/io/mifos/products/spi/PatternFactory.java
+++ b/service/src/main/java/io/mifos/products/spi/PatternFactory.java
@@ -18,27 +18,27 @@
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.Payment;
import java.math.BigDecimal;
import java.time.LocalDateTime;
-import java.util.List;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Stream;
/**
* @author Myrle Krantz
*/
public interface PatternFactory {
Pattern pattern();
- List<ChargeDefinition> charges();
+ Stream<ChargeDefinition> defaultConfigurableCharges();
void checkParameters(String parameters);
void persistParameters(Long caseId, String parameters);
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/main/resources/db/migrations/mariadb/V9__arrears_determination.sql b/service/src/main/resources/db/migrations/mariadb/V9__arrears_determination.sql
new file mode 100644
index 0000000..9fd882f
--- /dev/null
+++ b/service/src/main/resources/db/migrations/mariadb/V9__arrears_determination.sql
@@ -0,0 +1,39 @@
+--
+-- 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.
+--
+
+CREATE TABLE bastet_case_commands (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ case_id BIGINT NOT NULL,
+ action_name VARCHAR(32) NOT NULL,
+ created_on TIMESTAMP(3) NOT NULL,
+ created_by VARCHAR(32) NOT NULL,
+ thoth_transaction_uq VARCHAR(26) NOT NULL,
+
+ CONSTRAINT bastet_case_commands_pk PRIMARY KEY (id),
+ CONSTRAINT bastet_case_commands_uq UNIQUE (thoth_transaction_uq, action_name, case_id),
+ CONSTRAINT bastet_case_commands_fk FOREIGN KEY (case_id) REFERENCES bastet_cases (id)
+);
+
+CREATE TABLE bastet_p_arrears_config (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ product_id BIGINT NOT NULL,
+ days_late INT NOT NULL,
+ percent_provision DECIMAL(5,2) NOT NULL,
+
+ CONSTRAINT bastet_p_arrears_config_pk PRIMARY KEY (id),
+ CONSTRAINT bastet_p_arrears_config_uq UNIQUE (product_id, days_late),
+ CONSTRAINT bastet_p_arrears_config_fk FOREIGN KEY (product_id) REFERENCES bastet_products (id)
+);
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/DefaultChargeDefinitionsMocker.java b/service/src/test/java/io/mifos/individuallending/internal/service/DefaultChargeDefinitionsMocker.java
new file mode 100644
index 0000000..241876f
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/DefaultChargeDefinitionsMocker.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import org.mockito.Mockito;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class DefaultChargeDefinitionsMocker {
+ private static Stream<ChargeDefinition> charges() {
+ return Stream.concat(ChargeDefinitionService.defaultConfigurableIndividualLoanCharges(),
+ ChargeDefinitionService.individualLoanChargesDerivedFromConfiguration());
+ }
+
+ public static ChargeDefinitionService getChargeDefinitionService(final List<ChargeDefinition> changedCharges) {
+ final Map<String, ChargeDefinition> changedChargesMap = changedCharges.stream()
+ .collect(Collectors.toMap(ChargeDefinition::getIdentifier, x -> x));
+
+ final List<ChargeDefinition> defaultChargesWithFeesReplaced =
+ charges().map(x -> changedChargesMap.getOrDefault(x.getIdentifier(), x))
+ .collect(Collectors.toList());
+
+
+ final ChargeDefinitionService configurableChargeDefinitionServiceMock = Mockito.mock(ChargeDefinitionService.class);
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsByChargeAction = defaultChargesWithFeesReplaced.stream()
+ .collect(Collectors.groupingBy(ChargeDefinition::getChargeAction,
+ Collectors.mapping(x -> x, Collectors.toList())));
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsByAccrueAction = defaultChargesWithFeesReplaced.stream()
+ .filter(x -> x.getAccrueAction() != null)
+ .collect(Collectors.groupingBy(ChargeDefinition::getAccrueAction,
+ Collectors.mapping(x -> x, Collectors.toList())));
+ Mockito.doReturn(chargeDefinitionsByChargeAction).when(configurableChargeDefinitionServiceMock).getChargeDefinitionsMappedByChargeAction(Mockito.any());
+ Mockito.doReturn(chargeDefinitionsByAccrueAction).when(configurableChargeDefinitionServiceMock).getChargeDefinitionsMappedByAccrueAction(Mockito.any());
+
+ return configurableChargeDefinitionServiceMock;
+ }
+}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapperTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapperTest.java
new file mode 100644
index 0000000..be8a427
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapperTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.api.v1.domain.AccountAssignment;
+import io.mifos.portfolio.service.internal.repository.CaseAccountAssignmentEntity;
+import io.mifos.portfolio.service.internal.repository.ProductAccountAssignmentEntity;
+import io.mifos.portfolio.service.internal.util.AccountingAdapter;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author Myrle Krantz
+ */
+@RunWith(Parameterized.class)
+public class DesignatorToAccountIdentifierMapperTest {
+
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ static private class TestCase {
+ final String description;
+ Set<ProductAccountAssignmentEntity> productAccountAssignments;
+ Set<CaseAccountAssignmentEntity> caseAccountAssignments;
+ List<AccountAssignment> oneTimeAccountAssignments;
+ Set<AccountAssignment> expectedLedgersNeedingAccounts;
+ Optional<AccountAssignment> expectedCaseAccountAssignmentMappingForCustomerLoanGroup;
+ Set<DesignatorToAccountIdentifierMapper.GroupNeedingLedger> expectedGroupsNeedingLedgers;
+ Optional<String> expectedMapCustomerLoanPrincipalResult = Optional.empty();
+
+ private TestCase(String description) {
+ this.description = description;
+ }
+
+ TestCase productAccountAssignments(Set<ProductAccountAssignmentEntity> newVal) {
+ this.productAccountAssignments = newVal;
+ return this;
+ }
+
+ TestCase caseAccountAssignments(Set<CaseAccountAssignmentEntity> newVal) {
+ this.caseAccountAssignments = newVal;
+ return this;
+ }
+
+ TestCase oneTimeAccountAssignments(List<AccountAssignment> newVal) {
+ this.oneTimeAccountAssignments = newVal;
+ return this;
+ }
+
+ TestCase expectedLedgersNeedingAccounts(Set<AccountAssignment> newVal) {
+ this.expectedLedgersNeedingAccounts = newVal;
+ return this;
+ }
+
+ TestCase expectedCaseAccountAssignmentMappingForCustomerLoanGroup(Optional<AccountAssignment> newVal) {
+ this.expectedCaseAccountAssignmentMappingForCustomerLoanGroup = newVal;
+ return this;
+ }
+
+ TestCase expectedGroupsNeedingLedgers(Set<DesignatorToAccountIdentifierMapper.GroupNeedingLedger> newVal) {
+ this.expectedGroupsNeedingLedgers = newVal;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "TestCase{" +
+ "description='" + description + '\'' +
+ '}';
+ }
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<TestCase> ret = new ArrayList<>();
+ final TestCase groupedTestCase = new TestCase("basic grouped customer loan assignments")
+ .productAccountAssignments(new HashSet<>(Arrays.asList(
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, "x"),
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_INTEREST, "x"),
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_FEES, "x")
+ )))
+ .caseAccountAssignments(new HashSet<>(Collections.singletonList(
+ cAssignLedger(AccountDesignators.CUSTOMER_LOAN_GROUP)
+ )))
+ .oneTimeAccountAssignments(Collections.emptyList())
+ .expectedLedgersNeedingAccounts(new HashSet<>(Arrays.asList(
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, "y"),
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_INTEREST, "y"),
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_FEES, "y"))))
+ .expectedCaseAccountAssignmentMappingForCustomerLoanGroup(
+ Optional.of(assignAccount(AccountDesignators.CUSTOMER_LOAN_GROUP)))
+ .expectedGroupsNeedingLedgers(new HashSet<>(Collections.singletonList(
+ new DesignatorToAccountIdentifierMapper.GroupNeedingLedger(AccountDesignators.CUSTOMER_LOAN_GROUP, "x"))));
+ ret.add(groupedTestCase);
+
+ final TestCase groupingIgnoredTestCase = new TestCase("customer loan assignments with ignored grouping")
+ .productAccountAssignments(new HashSet<>(Arrays.asList(
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, "x"),
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_INTEREST, "y"),
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_FEES, "z")
+ )))
+ .caseAccountAssignments(Collections.emptySet())
+ .oneTimeAccountAssignments(Collections.emptyList())
+ .expectedLedgersNeedingAccounts(new HashSet<>(Arrays.asList(
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, "x"),
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_INTEREST, "y"),
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_FEES, "z"))))
+ .expectedCaseAccountAssignmentMappingForCustomerLoanGroup(
+ Optional.empty())
+ .expectedGroupsNeedingLedgers(Collections.emptySet());
+ ret.add(groupingIgnoredTestCase);
+ return ret;
+ }
+
+ private static ProductAccountAssignmentEntity pAssignLedger(
+ final String accountDesignator,
+ final String ledgerIdentifier) {
+ final ProductAccountAssignmentEntity ret = new ProductAccountAssignmentEntity();
+ ret.setDesignator(accountDesignator);
+ ret.setIdentifier(ledgerIdentifier);
+ ret.setType(AccountingAdapter.IdentifierType.LEDGER);
+ return ret;
+ }
+
+ private static CaseAccountAssignmentEntity cAssignLedger(
+ final String accountDesignator) {
+ final CaseAccountAssignmentEntity ret = new CaseAccountAssignmentEntity();
+ ret.setDesignator(accountDesignator);
+ ret.setIdentifier("y");
+ return ret;
+ }
+
+ private static AccountAssignment assignLedger(
+ final String accountDesignator,
+ final String ledgerIdentifier) {
+ final AccountAssignment ret = new AccountAssignment();
+ ret.setDesignator(accountDesignator);
+ ret.setLedgerIdentifier(ledgerIdentifier);
+ return ret;
+ }
+
+ private static AccountAssignment assignAccount(
+ final String accountDesignator) {
+ final AccountAssignment ret = new AccountAssignment();
+ ret.setDesignator(accountDesignator);
+ ret.setAccountIdentifier("y");
+ return ret;
+ }
+
+ private final TestCase testCase;
+ private final DesignatorToAccountIdentifierMapper testSubject;
+
+ public DesignatorToAccountIdentifierMapperTest(TestCase testCase) {
+ this.testCase = testCase;
+ this.testSubject = new DesignatorToAccountIdentifierMapper(
+ testCase.productAccountAssignments,
+ testCase.caseAccountAssignments,
+ testCase.oneTimeAccountAssignments);
+ }
+
+ @Test
+ public void map() {
+ Assert.assertEquals(Optional.empty(), testSubject.map(AccountDesignators.CUSTOMER_LOAN_GROUP));
+ Assert.assertEquals(testCase.expectedMapCustomerLoanPrincipalResult, testSubject.map(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL));
+ Assert.assertEquals(Optional.empty(), testSubject.map("this-account-designator-doesnt-exist"));
+ }
+
+ @Test
+ public void mapToCaseAccountAssignment() {
+ final Optional<AccountAssignment> ret = testSubject.mapToCaseAccountAssignment(AccountDesignators.CUSTOMER_LOAN_GROUP);
+ Assert.assertEquals(testCase.expectedCaseAccountAssignmentMappingForCustomerLoanGroup, ret);
+ }
+
+ @Test
+ public void getLedgersNeedingAccounts() {
+ final Set<AccountAssignment> ret = testSubject.getLedgersNeedingAccounts().collect(Collectors.toSet());
+ Assert.assertEquals(testCase.expectedLedgersNeedingAccounts, ret);
+ }
+
+ @Test
+ public void getGroupsNeedingLedgers() {
+ Set<DesignatorToAccountIdentifierMapper.GroupNeedingLedger> ret = testSubject.getGroupsNeedingLedgers().collect(Collectors.toSet());
+ //noinspection ResultOfMethodCallIgnored //Checking GroupNeedingLedger.toString that it doesn't cause exceptions.
+ ret.toString();
+ Assert.assertEquals(testCase.expectedGroupsNeedingLedgers, ret);
+ }
+}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java b/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
index 13c7ff3..9ecbd6e 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
@@ -18,6 +18,9 @@
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.service.schedule.Period;
+import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.PaymentCycle;
import io.mifos.portfolio.api.v1.domain.TermRange;
@@ -36,11 +39,11 @@
/**
* @author Myrle Krantz
*/
-class Fixture {
+public class Fixture {
- static CaseParameters getTestCaseParameters()
+ public static CaseParameters getTestCaseParameters()
{
- final CaseParameters ret = new CaseParameters(generateRandomIdentifier("fred"));
+ final CaseParameters ret = new CaseParameters(generateRandomIdentifier());
ret.setMaximumBalance(fixScale(BigDecimal.valueOf(2000L)));
ret.setTermRange(new TermRange(ChronoUnit.DAYS, 2));
@@ -49,9 +52,9 @@
return ret;
}
- private static String generateRandomIdentifier(final String prefix) {
+ private static String generateRandomIdentifier() {
//prefix followed by a random positive number with less than 4 digits.
- return prefix + Math.floorMod(Math.abs(new Random().nextInt()), 1000);
+ return "fred" + Math.floorMod(Math.abs(new Random().nextInt()), 1000);
}
private static BigDecimal fixScale(final BigDecimal bigDecimal)
{
@@ -59,10 +62,10 @@
}
- static ScheduledAction scheduledInterestAction(
- final LocalDate initialDisbursementDate,
- final int daysIn,
- final Period repaymentPeriod)
+ public static ScheduledAction scheduledInterestAction(
+ final LocalDate initialDisbursementDate,
+ final int daysIn,
+ final Period repaymentPeriod)
{
Assert.assertTrue(daysIn >= 1);
final LocalDate when = initialDisbursementDate.plusDays(daysIn);
@@ -70,7 +73,7 @@
return new ScheduledAction(Action.APPLY_INTEREST, when, actionPeriod, repaymentPeriod);
}
- static List<ScheduledAction> scheduledRepaymentActions(final LocalDate initial, final LocalDate... paymentDates)
+ public static List<ScheduledAction> scheduledRepaymentActions(final LocalDate initial, final LocalDate... paymentDates)
{
final List<ScheduledAction> ret = new ArrayList<>();
LocalDate begin = initial;
@@ -86,7 +89,7 @@
return new ScheduledAction(Action.ACCEPT_PAYMENT, to, repaymentPeriod, repaymentPeriod);
}
- static ScheduledCharge scheduledInterestBookingCharge(
+ public static ScheduledCharge scheduledInterestBookingCharge(
final LocalDate initialDate,
final int chargeDateDelta,
final int periodBeginDelta,
@@ -105,13 +108,13 @@
chargeDefinition.setAccrueAction(Action.APPLY_INTEREST.name());
chargeDefinition.setChargeAction(Action.ACCEPT_PAYMENT.name());
chargeDefinition.setAmount(BigDecimal.ONE);
- chargeDefinition.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN);
+ chargeDefinition.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN_INTEREST);
chargeDefinition.setAccrualAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
chargeDefinition.setToAccountDesignator(AccountDesignators.INTEREST_INCOME);
return new ScheduledCharge(scheduledAction, chargeDefinition, Optional.empty());
}
- static Period getPeriod(final LocalDate initialDate, final int periodBeginDelta, final int periodLength) {
+ public static Period getPeriod(final LocalDate initialDate, final int periodBeginDelta, final int periodLength) {
return new Period(initialDate.plusDays(periodBeginDelta), periodLength);
}
}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
index 4daaedc..91b18f5 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
@@ -15,7 +15,6 @@
*/
package io.mifos.individuallending.internal.service;
-import io.mifos.individuallending.IndividualLendingPatternFactory;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.caseinstance.ChargeName;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
@@ -24,11 +23,17 @@
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.individuallending.internal.service.schedule.ScheduledAction;
+import io.mifos.individuallending.internal.service.schedule.ScheduledActionHelpers;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
+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;
-import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -90,23 +95,21 @@
private int minorCurrencyUnitDigits = 2;
private CaseParameters caseParameters;
private LocalDate initialDisbursementDate;
- private List<ChargeDefinition> chargeDefinitions;
+ private List<ChargeDefinition> chargeDefinitions = Collections.emptyList();
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,
+ REPAY_PRINCIPAL_ID,
+ REPAY_FEES_ID,
+ REPAY_INTEREST_ID,
DISBURSE_PAYMENT_ID,
LATE_FEE_ID
));
private Map<ActionDatePair, List<ChargeDefinition>> chargeDefinitionsForActions = new HashMap<>();
- //This is an abuse of the ChargeInstance since everywhere else it's intended to contain account identifiers and not
+ //This is an abuse of the ChargeDefinition since everywhere else it's intended to contain account identifiers and not
//account designators. Don't copy the code around charge instances in this test without thinking about what you're
//doing carefully first.
@@ -154,7 +157,11 @@
final CaseEntity customerCase = new CaseEntity();
customerCase.setInterest(interest);
- return new DataContextOfAction(product, customerCase, CaseParametersMapper.map(1L, caseParameters), Collections.emptyList());
+ return new DataContextOfAction(
+ product,
+ customerCase,
+ CaseParametersMapper.map(1L, caseParameters),
+ Collections.emptyList());
}
@Override
@@ -177,8 +184,6 @@
private final TestCase testCase;
private final IndividualLoanService testSubject;
private final ScheduledChargesService scheduledChargesService;
- private final Map<String, List<ChargeDefinition>> chargeDefinitionsByChargeAction;
- private final Map<String, List<ChargeDefinition>> chargeDefinitionsByAccrueAction;
private static TestCase simpleCase()
@@ -189,29 +194,17 @@
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 List<ChargeDefinition> defaultChargesWithFeesReplaced =
- charges().stream().map(x -> {
- switch (x.getIdentifier()) {
- case PROCESSING_FEE_ID:
- return processingFeeCharge;
- case LOAN_ORIGINATION_FEE_ID:
- return loanOriginationFeeCharge;
- default:
- return x;
- }
- }).collect(Collectors.toList());
+ 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);
+
return new TestCase("simpleCase")
.minorCurrencyUnitDigits(2)
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
- .chargeDefinitions(defaultChargesWithFeesReplaced)
+ .chargeDefinitions(Arrays.asList(processingFeeCharge, loanOriginationFeeCharge))
.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()
@@ -223,13 +216,11 @@
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 1, 0, null, null));
caseParameters.setMaximumBalance(BigDecimal.valueOf(200000));
- final List<ChargeDefinition> charges = charges();
return new TestCase("yearLoanTestCase")
.minorCurrencyUnitDigits(3)
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
- .chargeDefinitions(charges)
.interest(BigDecimal.valueOf(10));
}
@@ -241,20 +232,14 @@
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, 1, 0, 0));
caseParameters.setMaximumBalance(BigDecimal.valueOf(2000));
- final List<ChargeDefinition> charges = charges();
return new TestCase("chargeDefaultsCase")
.minorCurrencyUnitDigits(2)
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
- .chargeDefinitions(charges)
.interest(BigDecimal.valueOf(5));
}
- private static List<ChargeDefinition> charges() {
- return IndividualLendingPatternFactory.defaultIndividualLoanCharges();
- }
-
private static ChargeDefinition getFixedSingleChargeDefinition(
final double amount,
final Action action,
@@ -267,7 +252,7 @@
ret.setChargeAction(action.name());
ret.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
ret.setProportionalTo(null);
- ret.setFromAccountDesignator(AccountDesignators.ENTRY);
+ ret.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN_FEES);
ret.setToAccountDesignator(feeAccountDesignator);
ret.setForCycleSizeUnit(null);
return ret;
@@ -277,21 +262,10 @@
{
this.testCase = testCase;
- final ChargeDefinitionService chargeDefinitionServiceMock = Mockito.mock(ChargeDefinitionService.class);
- chargeDefinitionsByChargeAction = testCase.chargeDefinitions.stream()
- .collect(Collectors.groupingBy(ChargeDefinition::getChargeAction,
- Collectors.mapping(x -> x, Collectors.toList())));
- chargeDefinitionsByAccrueAction = testCase.chargeDefinitions.stream()
- .filter(x -> x.getAccrueAction() != null)
- .collect(Collectors.groupingBy(ChargeDefinition::getAccrueAction,
- Collectors.mapping(x -> x, Collectors.toList())));
- Mockito.doReturn(chargeDefinitionsByChargeAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByChargeAction(testCase.productIdentifier);
- Mockito.doReturn(chargeDefinitionsByAccrueAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByAccrueAction(testCase.productIdentifier);
-
final BalanceSegmentRepository balanceSegmentRepositoryMock = Mockito.mock(BalanceSegmentRepository.class);
Mockito.doReturn(Stream.empty()).when(balanceSegmentRepositoryMock).findByProductIdentifierAndSegmentSetIdentifier(Matchers.anyString(), Matchers.anyString());
- scheduledChargesService = new ScheduledChargesService(chargeDefinitionServiceMock, balanceSegmentRepositoryMock);
+ scheduledChargesService = new ScheduledChargesService(DefaultChargeDefinitionsMocker.getChargeDefinitionService(testCase.chargeDefinitions), balanceSegmentRepositoryMock);
testSubject = new IndividualLoanService(scheduledChargesService);
}
@@ -318,38 +292,69 @@
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 valueOfRepayPrincipalCostComponent = allPlannedPayments.get(x).getPayment().getCostComponents().stream()
+ .filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.REPAY_PRINCIPAL_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 valueOfRepayFeeCostComponent = allPlannedPayments.get(x).getPayment().getCostComponents().stream()
+ .filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.REPAY_FEES_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 valueOfRepayInterestCostComponent = allPlannedPayments.get(x).getPayment().getCostComponents().stream()
+ .filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.REPAY_INTEREST_ID))
+ .map(CostComponent::getAmount)
+ .reduce(BigDecimal::add)
+ .orElse(BigDecimal.ZERO);
+ 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 interestAccrualBalance = allPlannedPayments.get(x).getPayment().getBalanceAdjustments().getOrDefault(AccountDesignators.INTEREST_ACCRUAL, BigDecimal.ZERO);
+ final BigDecimal lateFeeAccrualBalance = allPlannedPayments.get(x).getPayment().getBalanceAdjustments().getOrDefault(AccountDesignators.LATE_FEE_ACCRUAL, BigDecimal.ZERO);
+ final BigDecimal principalDifference =
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, x - 1)
+ .subtract(
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, x));
+ Assert.assertEquals(valueOfRepayInterestCostComponent, valueOfInterestCostComponent);
+ Assert.assertEquals(BigDecimal.ZERO, interestAccrualBalance);
+ Assert.assertEquals(BigDecimal.ZERO, lateFeeAccrualBalance);
+ Assert.assertEquals("Checking payment " + x, valueOfRepayPrincipalCostComponent, 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));
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, x).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 valueOfRepayPrincipalCostComponent.add(valueOfRepayInterestCostComponent).add(valueOfRepayFeeCostComponent);
}
).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_PRINCIPAL).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 principal balance should be zero.",
+ BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, allPlannedPayments.size() - 1));
+
+ Assert.assertEquals("Final interest balance should be zero.",
+ BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_INTEREST, allPlannedPayments.size() - 1));
+
+ Assert.assertEquals("Final fees balance should be zero.",
+ BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_FEES, allPlannedPayments.size() - 1));
+
//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 +364,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,20 +372,11 @@
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;
-
- }
+ private BigDecimal getBalanceForPayment(
+ final List<PlannedPayment> allPlannedPayments,
+ final String accountDesignator,
+ int index) {
+ return allPlannedPayments.get(index).getBalances().get(accountDesignator);
}
@Test
@@ -394,8 +386,8 @@
scheduledActions);
final List<LocalDate> interestCalculationDates = scheduledCharges.stream()
- .filter(scheduledCharge -> scheduledCharge.getScheduledAction().action == Action.APPLY_INTEREST)
- .map(scheduledCharge -> scheduledCharge.getScheduledAction().when)
+ .filter(scheduledCharge -> scheduledCharge.getScheduledAction().getAction() == Action.APPLY_INTEREST)
+ .map(scheduledCharge -> scheduledCharge.getScheduledAction().getWhen())
.collect(Collectors.toList());
final List<LocalDate> allTheDaysAfterTheInitialDisbursementDate
@@ -405,25 +397,26 @@
Assert.assertEquals(interestCalculationDates, allTheDaysAfterTheInitialDisbursementDate);
- final List<LocalDate> acceptPaymentDates = scheduledCharges.stream()
- .filter(scheduledCharge -> scheduledCharge.getScheduledAction().action == Action.ACCEPT_PAYMENT)
- .map(scheduledCharge -> scheduledCharge.getScheduledAction().when)
+ /*final List<LocalDate> acceptPaymentDates = scheduledCharges.stream()
+ .filter(scheduledCharge -> scheduledCharge.getScheduledAction().getAction() == Action.ACCEPT_PAYMENT)
+ .map(scheduledCharge -> scheduledCharge.getScheduledAction().getWhen())
.collect(Collectors.toList());
final long expectedAcceptPayments = scheduledActions.stream()
- .filter(x -> x.action == Action.ACCEPT_PAYMENT).count();
+ .filter(x -> x.getAction() == Action.ACCEPT_PAYMENT).count();
final List<ChargeDefinition> chargeDefinitionsMappedToAcceptPayment = chargeDefinitionsByChargeAction.get(Action.ACCEPT_PAYMENT.name());
final int numberOfChangeDefinitionsMappedToAcceptPayment = chargeDefinitionsMappedToAcceptPayment == null ? 0 : chargeDefinitionsMappedToAcceptPayment.size();
Assert.assertEquals("check for correct number of scheduled charges for accept payment",
expectedAcceptPayments*numberOfChangeDefinitionsMappedToAcceptPayment,
- acceptPaymentDates.size());
+ acceptPaymentDates.size());*/
final Map<ActionDatePair, Set<ChargeDefinition>> searchableScheduledCharges = scheduledCharges.stream()
.collect(
Collectors.groupingBy(scheduledCharge ->
- new ActionDatePair(scheduledCharge.getScheduledAction().action, scheduledCharge.getScheduledAction().when),
+ new ActionDatePair(scheduledCharge.getScheduledAction().getAction(), scheduledCharge.getScheduledAction().getWhen()),
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/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java
new file mode 100644
index 0000000..905f1f8
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
+import io.mifos.portfolio.api.v1.domain.Payment;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+public class AcceptPaymentBuilderServiceTest {
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<PaymentBuilderServiceTestCase> ret = new ArrayList<>();
+ ret.add(simpleCase());
+ return ret;
+ }
+
+ private static PaymentBuilderServiceTestCase simpleCase() {
+ final PaymentBuilderServiceTestCase testCase = new PaymentBuilderServiceTestCase("simple case");
+ testCase.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, testCase.balance.negate());
+ testCase.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_INTEREST, testCase.accruedInterest.negate());
+ testCase.runningBalances.adjustBalance(AccountDesignators.INTEREST_ACCRUAL, testCase.accruedInterest);
+ return testCase;
+ }
+
+ private final PaymentBuilderServiceTestCase testCase;
+
+ public AcceptPaymentBuilderServiceTest(final PaymentBuilderServiceTestCase testCase) {
+ this.testCase = testCase;
+ }
+
+ @Test
+ public void getPaymentBuilder() throws Exception {
+ final PaymentBuilder paymentBuilder = PaymentBuilderServiceTestHarness.constructCallToPaymentBuilder(
+ AcceptPaymentBuilderService::new, testCase);
+
+ final Payment payment = paymentBuilder.buildPayment(Action.ACCEPT_PAYMENT, Collections.emptySet(), testCase.forDate.toLocalDate());
+ Assert.assertNotNull(payment);
+ final Map<String, CostComponent> mappedCostComponents = payment.getCostComponents().stream()
+ .collect(Collectors.toMap(CostComponent::getChargeIdentifier, x -> x));
+
+ Assert.assertEquals(testCase.accruedInterest, mappedCostComponents.get(ChargeIdentifiers.INTEREST_ID).getAmount());
+ Assert.assertEquals(testCase.accruedInterest, mappedCostComponents.get(ChargeIdentifiers.REPAY_INTEREST_ID).getAmount());
+ Assert.assertEquals(testCase.paymentSize.subtract(testCase.accruedInterest), mappedCostComponents.get(ChargeIdentifiers.REPAY_PRINCIPAL_ID).getAmount());
+ }
+}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java
new file mode 100644
index 0000000..7771678
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.portfolio.api.v1.domain.Payment;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+public class ApplyInterestPaymentBuilderServiceTest {
+ @Test
+ public void getPaymentBuilder() throws Exception {
+ final PaymentBuilderServiceTestCase testCase = new PaymentBuilderServiceTestCase("simple case");
+ testCase.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, testCase.balance.negate());
+
+ final PaymentBuilder paymentBuilder = PaymentBuilderServiceTestHarness.constructCallToPaymentBuilder(
+ ApplyInterestPaymentBuilderService::new, testCase);
+
+ final Payment payment = paymentBuilder.buildPayment(Action.APPLY_INTEREST, Collections.emptySet(), testCase.forDate.toLocalDate());
+ Assert.assertNotNull(payment);
+
+ Assert.assertEquals(BigDecimal.valueOf(27, 2), paymentBuilder.getBalanceAdjustments().get(AccountDesignators.INTEREST_ACCRUAL));
+ }
+}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentServiceTest.java
similarity index 72%
rename from service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
rename to service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentServiceTest.java
index f2aa460..f518ec4 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/CostComponentServiceTest.java
@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.costcomponent;
+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,19 @@
@Test
public void getAmountProportionalTo() {
+ final SimulatedRunningBalances runningBalances = new SimulatedRunningBalances();
+ runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, testCase.runningBalance.negate());
final BigDecimal amount = CostComponentService.getAmountProportionalTo(
+ null,
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/costcomponent/PaymentBuilderServiceTestCase.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestCase.java
new file mode 100644
index 0000000..76bae20
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestCase.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+class PaymentBuilderServiceTestCase {
+ private final String description;
+ private LocalDateTime startOfTerm = LocalDateTime.of(2015, 1, 15, 0, 0);
+ LocalDateTime endOfTerm = LocalDate.of(2015, 8, 15).atStartOfDay();
+ LocalDateTime forDate = startOfTerm.plusMonths(1);
+ BigDecimal paymentSize = BigDecimal.valueOf(100_00, 2);
+ BigDecimal balance = BigDecimal.valueOf(2000_00, 2);
+ BigDecimal balanceRangeMaximum = BigDecimal.valueOf(1000_00, 2);
+ BigDecimal interestRate = BigDecimal.valueOf(5_00, 2);
+ BigDecimal accruedInterest = BigDecimal.valueOf(10_00, 2);
+ SimulatedRunningBalances runningBalances;
+
+ PaymentBuilderServiceTestCase(final String description) {
+ this.description = description;
+ runningBalances = new SimulatedRunningBalances(startOfTerm);
+ }
+
+ PaymentBuilderServiceTestCase endOfTerm(LocalDateTime endOfTerm) {
+ this.endOfTerm = endOfTerm;
+ return this;
+ }
+
+ PaymentBuilderServiceTestCase forDate(LocalDateTime forDate) {
+ this.forDate = forDate;
+ return this;
+ }
+
+ PaymentBuilderServiceTestCase paymentSize(BigDecimal paymentSize) {
+ this.paymentSize = paymentSize;
+ return this;
+ }
+
+ PaymentBuilderServiceTestCase balance(BigDecimal balance) {
+ this.balance = balance;
+ return this;
+ }
+
+ PaymentBuilderServiceTestCase balanceRangeMaximum(BigDecimal balanceRangeMaximum) {
+ this.balanceRangeMaximum = balanceRangeMaximum;
+ return this;
+ }
+
+ PaymentBuilderServiceTestCase interestRate(BigDecimal interestRate) {
+ this.interestRate = interestRate;
+ return this;
+ }
+
+ PaymentBuilderServiceTestCase accruedInterest(BigDecimal accruedInterest) {
+ this.accruedInterest = accruedInterest;
+ return this;
+ }
+
+ PaymentBuilderServiceTestCase runningBalances(SimulatedRunningBalances newVal) {
+ this.runningBalances = newVal;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "PaymentBuilderServiceTestCase{" +
+ "description='" + description + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestHarness.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestHarness.java
new file mode 100644
index 0000000..99a5c17
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderServiceTestHarness.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.internal.service.costcomponent;
+
+import io.mifos.individuallending.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.ChargeDefinitionService;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.DefaultChargeDefinitionsMocker;
+import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
+import io.mifos.portfolio.service.internal.repository.CaseEntity;
+import io.mifos.portfolio.service.internal.repository.ProductEntity;
+import org.mockito.Mockito;
+
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.function.Function;
+
+class PaymentBuilderServiceTestHarness {
+ static PaymentBuilder constructCallToPaymentBuilder (
+ final Function<ScheduledChargesService, PaymentBuilderService> serviceFactory,
+ final PaymentBuilderServiceTestCase testCase) {
+ final BalanceSegmentRepository balanceSegmentRepository = Mockito.mock(BalanceSegmentRepository.class);
+ final ChargeDefinitionService chargeDefinitionService = DefaultChargeDefinitionsMocker.getChargeDefinitionService(Collections.emptyList());
+ final ScheduledChargesService scheduledChargesService = new ScheduledChargesService(chargeDefinitionService, balanceSegmentRepository);
+ final PaymentBuilderService testSubject = serviceFactory.apply(scheduledChargesService);
+
+ final ProductEntity product = new ProductEntity();
+ product.setIdentifier("blah");
+ product.setMinorCurrencyUnitDigits(2);
+ final CaseEntity customerCase = new CaseEntity();
+ customerCase.setEndOfTerm(testCase.endOfTerm);
+ customerCase.setInterest(testCase.interestRate);
+ final CaseParametersEntity caseParameters = new CaseParametersEntity();
+ caseParameters.setPaymentSize(testCase.paymentSize);
+ caseParameters.setBalanceRangeMaximum(testCase.balanceRangeMaximum);
+ caseParameters.setPaymentCyclePeriod(1);
+ caseParameters.setPaymentCycleTemporalUnit(ChronoUnit.MONTHS);
+ caseParameters.setCreditWorthinessFactors(Collections.emptySet());
+
+ final DataContextOfAction dataContextOfAction = new DataContextOfAction(
+ product,
+ customerCase,
+ caseParameters,
+ Collections.emptyList());
+ return testSubject.getPaymentBuilder(
+ dataContextOfAction,
+ testCase.paymentSize,
+ testCase.forDate.toLocalDate(),
+ testCase.runningBalances);
+ }
+}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderTest.java
new file mode 100644
index 0000000..2e000ed
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class PaymentBuilderTest {
+ @Test
+ public void expandAccountDesignators() {
+ final Set<String> ret = PaymentBuilder.expandAccountDesignators(new HashSet<>(Arrays.asList(AccountDesignators.CUSTOMER_LOAN_GROUP, AccountDesignators.ENTRY)));
+ final Set<String> expected = new HashSet<>(Arrays.asList(
+ AccountDesignators.ENTRY,
+ AccountDesignators.CUSTOMER_LOAN_GROUP,
+ AccountDesignators.CUSTOMER_LOAN_PRINCIPAL,
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountDesignators.CUSTOMER_LOAN_INTEREST));
+
+ Assert.assertEquals(expected, ret);
+ }
+
+}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/PeriodChargeCalculatorTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PeriodChargeCalculatorTest.java
similarity index 95%
rename from service/src/test/java/io/mifos/individuallending/internal/service/PeriodChargeCalculatorTest.java
rename to service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PeriodChargeCalculatorTest.java
index 71e7b42..15a3578 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/PeriodChargeCalculatorTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/PeriodChargeCalculatorTest.java
@@ -13,8 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.costcomponent;
+import io.mifos.individuallending.internal.service.schedule.Period;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -36,7 +38,7 @@
private static class TestCase {
final String description;
List<ScheduledCharge> scheduledCharges;
- int precision;
+ int precision = 20;
Map<Period, BigDecimal> expectedPeriodRates;
private BigDecimal interest;
@@ -49,11 +51,6 @@
return this;
}
- TestCase precision(final int newVal) {
- this.precision = newVal;
- return this;
- }
-
TestCase expectedPeriodRates(final Map<Period, BigDecimal> newVal) {
this.expectedPeriodRates = newVal;
return this;
@@ -98,7 +95,6 @@
return new TestCase("simpleCase")
.interest(BigDecimal.ONE)
.scheduledCharges(scheduledCharges)
- .precision(20)
.expectedPeriodRates(expectedPeriodRates);
}
@@ -119,7 +115,6 @@
return new TestCase("bitOfCompoundingCase")
.interest(BigDecimal.TEN)
.scheduledCharges(scheduledCharges)
- .precision(20)
.expectedPeriodRates(expectedPeriodRates);
}
@@ -137,7 +132,6 @@
return new TestCase("zeroInterestPerPeriod")
.interest(BigDecimal.ZERO)
.scheduledCharges(scheduledCharges)
- .precision(20)
.expectedPeriodRates(expectedPeriodRates);
}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ChargeRangeTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/ChargeRangeTest.java
similarity index 96%
rename from service/src/test/java/io/mifos/individuallending/internal/service/ChargeRangeTest.java
rename to service/src/test/java/io/mifos/individuallending/internal/service/schedule/ChargeRangeTest.java
index 14757d7..c44be6f 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ChargeRangeTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/ChargeRangeTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
import org.junit.Assert;
import org.junit.Test;
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/PeriodTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/PeriodTest.java
similarity index 95%
rename from service/src/test/java/io/mifos/individuallending/internal/service/PeriodTest.java
rename to service/src/test/java/io/mifos/individuallending/internal/service/schedule/PeriodTest.java
index 0235e48..a28d8c1 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/PeriodTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/PeriodTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
import org.junit.Assert;
import org.junit.BeforeClass;
@@ -66,6 +66,7 @@
final Period tommorrowPeriod = new Period(tommorrow, dayAfterTommorrow);
Assert.assertTrue(yesterdayPeriod.compareTo(todayPeriod) < 0);
+ //noinspection EqualsWithItself
Assert.assertTrue(todayPeriod.compareTo(todayPeriod) == 0);
Assert.assertTrue(tommorrowPeriod.compareTo(todayPeriod) > 0);
}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelpersTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledActionHelpersTest.java
similarity index 95%
rename from service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelpersTest.java
rename to service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledActionHelpersTest.java
index 61771b6..23eaa9a 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionHelpersTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledActionHelpersTest.java
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
-import io.mifos.portfolio.api.v1.domain.PaymentCycle;
-import io.mifos.portfolio.api.v1.domain.TermRange;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.portfolio.api.v1.domain.PaymentCycle;
+import io.mifos.portfolio.api.v1.domain.TermRange;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -371,19 +371,19 @@
Assert.assertTrue("Case " + testCase.description + " missing these expected results " + missingExpectedResults,
missingExpectedResults.isEmpty());
result.forEach(x -> {
- Assert.assertTrue(x.toString(), testCase.earliestActionDate.isBefore(x.when) || testCase.earliestActionDate.isEqual(x.when));
- Assert.assertTrue(x.toString(), testCase.latestActionDate.isAfter(x.when) || testCase.latestActionDate.isEqual(x.when));
+ Assert.assertTrue(x.toString(), testCase.earliestActionDate.isBefore(x.getWhen()) || testCase.earliestActionDate.isEqual(x.getWhen()));
+ Assert.assertTrue(x.toString(), testCase.latestActionDate.isAfter(x.getWhen()) || testCase.latestActionDate.isEqual(x.getWhen()));
});
Assert.assertEquals(testCase.expectedPaymentCount, countActionsByType(result, Action.ACCEPT_PAYMENT));
Assert.assertEquals(testCase.expectedInterestCount, countActionsByType(result, Action.APPLY_INTEREST));
Assert.assertEquals(1, countActionsByType(result, Action.APPROVE));
Assert.assertEquals(1, countActionsByType(result, Action.CLOSE));
- result.stream().filter(scheduledAction -> !ScheduledActionHelpers.actionHasNoActionPeriod(scheduledAction.action))
+ result.stream().filter(scheduledAction -> !ScheduledActionHelpers.actionHasNoActionPeriod(scheduledAction.getAction()))
.forEach(scheduledAction -> {
Assert.assertNotNull("The action period of " + scheduledAction.toString() + " should not be null.",
- scheduledAction.actionPeriod);
+ scheduledAction.getActionPeriod());
Assert.assertNotNull("The repayment period of " + scheduledAction.toString() + " should not be null.",
- scheduledAction.repaymentPeriod);
+ scheduledAction.getRepaymentPeriod());
});
Assert.assertTrue(noDuplicatesInResult(result));
Assert.assertTrue(maximumOneInterestPerDay(result));
@@ -394,11 +394,11 @@
final LocalDate roughEndDate = ScheduledActionHelpers.getRoughEndDate(testCase.initialDisbursementDate, testCase.caseParameters);
testCase.expectedResultContents.stream()
- .filter(x -> x.action == Action.ACCEPT_PAYMENT)
+ .filter(x -> x.getAction() == Action.ACCEPT_PAYMENT)
.forEach(expectedResultContents -> {
final ScheduledAction nextScheduledPayment = ScheduledActionHelpers.getNextScheduledPayment(
testCase.initialDisbursementDate,
- expectedResultContents.when.minusDays(1),
+ expectedResultContents.getWhen().minusDays(1),
roughEndDate,
testCase.caseParameters);
Assert.assertEquals(expectedResultContents, nextScheduledPayment);
@@ -410,18 +410,18 @@
roughEndDate,
testCase.caseParameters);
- Assert.assertNotNull(afterAction.actionPeriod);
- Assert.assertTrue(afterAction.actionPeriod.isLastPeriod());
+ Assert.assertNotNull(afterAction.getActionPeriod());
+ Assert.assertTrue(afterAction.getActionPeriod().isLastPeriod());
}
private long countActionsByType(final List<ScheduledAction> scheduledActions, final Action actionToCount) {
- return scheduledActions.stream().filter(x -> x.action == actionToCount).count();
+ return scheduledActions.stream().filter(x -> x.getAction() == actionToCount).count();
}
private boolean maximumOneInterestPerDay(final List<ScheduledAction> result) {
final List<LocalDate> interestDays = result.stream()
- .filter(x -> x.action == Action.APPLY_INTEREST)
- .map(x -> x.when)
+ .filter(x -> x.getAction() == Action.APPLY_INTEREST)
+ .map(ScheduledAction::getWhen)
.collect(Collectors.toList());
final Set<LocalDate> interestDaysSet = new HashSet<>();
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledActionTest.java
similarity index 95%
rename from service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java
rename to service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledActionTest.java
index a287c2f..a8f43ab 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledActionTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import org.junit.Assert;
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargeComparatorTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargeComparatorTest.java
similarity index 98%
rename from service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargeComparatorTest.java
rename to service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargeComparatorTest.java
index 2e38d67..a2acb4b 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargeComparatorTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargeComparatorTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargesServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargesServiceTest.java
similarity index 97%
rename from service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargesServiceTest.java
rename to service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargesServiceTest.java
index 57cee42..ad1f95c 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargesServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/schedule/ScheduledChargesServiceTest.java
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package io.mifos.individuallending.internal.service;
+package io.mifos.individuallending.internal.service.schedule;
+import io.mifos.individuallending.internal.service.ChargeDefinitionService;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.service.internal.repository.BalanceSegmentEntity;
import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
-import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/service/src/test/java/io/mifos/portfolio/service/internal/util/AccountingAdapterTest.java b/service/src/test/java/io/mifos/portfolio/service/internal/util/AccountingAdapterTest.java
new file mode 100644
index 0000000..69519bb
--- /dev/null
+++ b/service/src/test/java/io/mifos/portfolio/service/internal/util/AccountingAdapterTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.portfolio.service.internal.util;
+
+import com.google.common.collect.Sets;
+import io.mifos.accounting.api.v1.domain.Creditor;
+import io.mifos.accounting.api.v1.domain.Debtor;
+import io.mifos.accounting.api.v1.domain.JournalEntry;
+import io.mifos.individuallending.internal.service.DesignatorToAccountIdentifierMapper;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Myrle Krantz
+ */
+public class AccountingAdapterTest {
+ @Test
+ public void getJournalEntryWithMultipleIdenticalChargesMappedToSameAccount() {
+ final BigDecimal two = BigDecimal.valueOf(2);
+ final BigDecimal negativeTwo = two.negate();
+ final Map<String, BigDecimal> balanceAdjustments = new HashMap<>();
+ balanceAdjustments.put("a", BigDecimal.ONE);
+ balanceAdjustments.put("b", BigDecimal.ONE);
+ balanceAdjustments.put("c", negativeTwo);
+
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper = Mockito.mock(DesignatorToAccountIdentifierMapper.class);
+ Mockito.doReturn("a1").when(designatorToAccountIdentifierMapper).mapOrThrow("a");
+ Mockito.doReturn("a1").when(designatorToAccountIdentifierMapper).mapOrThrow("b");
+ Mockito.doReturn("c1").when(designatorToAccountIdentifierMapper).mapOrThrow("c");
+
+ final JournalEntry journalEntry = AccountingAdapter.getJournalEntry(
+ balanceAdjustments,
+ designatorToAccountIdentifierMapper,
+ "", "", "", "", "", "");
+ Assert.assertEquals(Sets.newHashSet(new Debtor("c1", two.toPlainString())), journalEntry.getDebtors());
+ Assert.assertEquals(Sets.newHashSet(new Creditor("a1", two.toPlainString())), journalEntry.getCreditors());
+ }
+
+}
\ No newline at end of file
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'