Merge pull request #5 from myrlen/develop
charge definitions which reference balance ranges.
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 f04545b..1adcc04 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
@@ -132,6 +132,48 @@
Boolean getProductEnabled(@PathVariable("productidentifier") final String productIdentifier);
@RequestMapping(
+ value = "/products/{productidentifier}/balancesegmentsets/",
+ method = RequestMethod.POST,
+ produces = MediaType.APPLICATION_JSON_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ThrowsException(status = HttpStatus.CONFLICT, exception = ProductInUseException.class)
+ void createBalanceSegmentSet(
+ @PathVariable("productidentifier") final String productIdentifier,
+ final BalanceSegmentSet balanceSegmentSet);
+
+ @RequestMapping(
+ value = "/products/{productidentifier}/balancesegmentsets/{balancesegmentsetidentifier}",
+ method = RequestMethod.GET,
+ produces = MediaType.ALL_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE)
+ BalanceSegmentSet getBalanceSegmentSet(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("balancesegmentsetidentifier") final String balanceSegmentSetIdentifier);
+
+ @RequestMapping(
+ value = "/products/{productidentifier}/balancesegmentsets/{balancesegmentsetidentifier}",
+ method = RequestMethod.PUT,
+ produces = MediaType.ALL_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ThrowsException(status = HttpStatus.CONFLICT, exception = ProductInUseException.class)
+ void changeBalanceSegmentSet(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("balancesegmentsetidentifier") final String balanceSegmentSetIdentifier,
+ BalanceSegmentSet balanceSegmentSet);
+
+ @RequestMapping(
+ value = "/products/{productidentifier}/balancesegmentsets/{balancesegmentsetidentifier}",
+ method = RequestMethod.DELETE,
+ produces = MediaType.ALL_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE
+ )
+ @ThrowsException(status = HttpStatus.CONFLICT, exception = ProductInUseException.class)
+ void deleteBalanceSegmentSet(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("balancesegmentsetidentifier") final String balanceSegmentSetIdentifier);
+
+ @RequestMapping(
value = "/products/{productidentifier}/tasks/",
method = RequestMethod.GET,
produces = MediaType.ALL_VALUE,
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/BalanceSegmentSet.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/BalanceSegmentSet.java
new file mode 100644
index 0000000..a2daf1c
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/BalanceSegmentSet.java
@@ -0,0 +1,89 @@
+/*
+ * 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 io.mifos.core.lang.validation.constraints.ValidIdentifier;
+import io.mifos.core.lang.validation.constraints.ValidIdentifiers;
+import io.mifos.portfolio.api.v1.validation.ValidSegmentList;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@ValidSegmentList
+public class BalanceSegmentSet {
+ @ValidIdentifier
+ private String identifier;
+
+ private List<BigDecimal> segments;
+
+ @ValidIdentifiers
+ private List<String> segmentIdentifiers;
+
+ public BalanceSegmentSet() {
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ public void setIdentifier(String identifier) {
+ this.identifier = identifier;
+ }
+
+ public List<BigDecimal> getSegments() {
+ return segments;
+ }
+
+ public void setSegments(List<BigDecimal> segments) {
+ this.segments = segments;
+ }
+
+ public List<String> getSegmentIdentifiers() {
+ return segmentIdentifiers;
+ }
+
+ public void setSegmentIdentifiers(List<String> segmentNames) {
+ this.segmentIdentifiers = segmentNames;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BalanceSegmentSet that = (BalanceSegmentSet) o;
+ return Objects.equals(identifier, that.identifier) &&
+ Objects.equals(segments, that.segments) &&
+ Objects.equals(segmentIdentifiers, that.segmentIdentifiers);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(identifier, segments, segmentIdentifiers);
+ }
+
+ @Override
+ public String toString() {
+ return "BalanceSegmentSet{" +
+ "identifier='" + identifier + '\'' +
+ ", segments=" + segments +
+ ", segmentIdentifiers=" + segmentIdentifiers +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
index b7170d0..3890867 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
@@ -15,11 +15,11 @@
*/
package io.mifos.portfolio.api.v1.domain;
+import io.mifos.core.lang.validation.constraints.ValidIdentifier;
+import io.mifos.portfolio.api.v1.validation.ValidChargeDefinition;
import io.mifos.portfolio.api.v1.validation.ValidChargeReference;
import io.mifos.portfolio.api.v1.validation.ValidPaymentCycleUnit;
-import io.mifos.core.lang.validation.constraints.ValidIdentifier;
import org.hibernate.validator.constraints.NotBlank;
-import org.hibernate.validator.constraints.ScriptAssert;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
@@ -31,10 +31,7 @@
* @author Myrle Krantz
*/
@SuppressWarnings({"unused", "WeakerAccess"})
-@ScriptAssert(lang = "javascript", script = "_this.amount !== null " +
- "&& _this.amount.scale() <= 4 " +
- "&& ((_this.accrueAction === null && _this.accrualAccountDesignator === null) || (_this.accrueAction !== null && _this.accrualAccountDesignator !== null))" +
- "&& ((_this.chargeMethod == 'PROPORTIONAL' && _this.proportionalTo !== null) || (_this.chargeMethod == 'FIXED' && _this.proportionalTo === null))")
+@ValidChargeDefinition
public class ChargeDefinition {
@SuppressWarnings("WeakerAccess")
public enum ChargeMethod {
@@ -82,6 +79,15 @@
private boolean readOnly;
+ @ValidIdentifier(optional = true)
+ private String forSegmentSet;
+
+ @ValidIdentifier(optional = true)
+ private String fromSegment;
+
+ @ValidIdentifier(optional = true)
+ private String toSegment;
+
public ChargeDefinition() {
}
@@ -190,6 +196,30 @@
this.readOnly = readOnly;
}
+ public String getForSegmentSet() {
+ return forSegmentSet;
+ }
+
+ public void setForSegmentSet(String forSegmentSet) {
+ this.forSegmentSet = forSegmentSet;
+ }
+
+ public String getFromSegment() {
+ return fromSegment;
+ }
+
+ public void setFromSegment(String fromSegment) {
+ this.fromSegment = fromSegment;
+ }
+
+ public String getToSegment() {
+ return toSegment;
+ }
+
+ public void setToSegment(String toSegment) {
+ this.toSegment = toSegment;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -207,12 +237,15 @@
Objects.equals(fromAccountDesignator, that.fromAccountDesignator) &&
Objects.equals(accrualAccountDesignator, that.accrualAccountDesignator) &&
Objects.equals(toAccountDesignator, that.toAccountDesignator) &&
- forCycleSizeUnit == that.forCycleSizeUnit;
+ forCycleSizeUnit == that.forCycleSizeUnit &&
+ Objects.equals(forSegmentSet, that.forSegmentSet) &&
+ Objects.equals(fromSegment, that.fromSegment) &&
+ Objects.equals(toSegment, that.toSegment);
}
@Override
public int hashCode() {
- return Objects.hash(identifier, name, description, accrueAction, chargeAction, amount, chargeMethod, proportionalTo, fromAccountDesignator, accrualAccountDesignator, toAccountDesignator, forCycleSizeUnit, readOnly);
+ return Objects.hash(identifier, name, description, accrueAction, chargeAction, amount, chargeMethod, proportionalTo, fromAccountDesignator, accrualAccountDesignator, toAccountDesignator, forCycleSizeUnit, readOnly, forSegmentSet, fromSegment, toSegment);
}
@Override
@@ -231,6 +264,9 @@
", toAccountDesignator='" + toAccountDesignator + '\'' +
", forCycleSizeUnit=" + forCycleSizeUnit +
", readOnly=" + readOnly +
+ ", forSegmentSet='" + forSegmentSet + '\'' +
+ ", fromSegment='" + fromSegment + '\'' +
+ ", toSegment='" + toSegment + '\'' +
'}';
}
}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/events/BalanceSegmentSetEvent.java b/api/src/main/java/io/mifos/portfolio/api/v1/events/BalanceSegmentSetEvent.java
new file mode 100644
index 0000000..8cbaf75
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/events/BalanceSegmentSetEvent.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.portfolio.api.v1.events;
+
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+public class BalanceSegmentSetEvent {
+ private String productIdentifier;
+ private String balanceSegmentSetIdentifier;
+
+ public BalanceSegmentSetEvent() {
+ }
+
+ public BalanceSegmentSetEvent(String productIdentifier, String balanceSegmentSetIdentifier) {
+ this.productIdentifier = productIdentifier;
+ this.balanceSegmentSetIdentifier = balanceSegmentSetIdentifier;
+ }
+
+ public String getProductIdentifier() {
+ return productIdentifier;
+ }
+
+ public void setProductIdentifier(String productIdentifier) {
+ this.productIdentifier = productIdentifier;
+ }
+
+ public String getBalanceSegmentSetIdentifier() {
+ return balanceSegmentSetIdentifier;
+ }
+
+ public void setBalanceSegmentSetIdentifier(String balanceSegmentSetIdentifier) {
+ this.balanceSegmentSetIdentifier = balanceSegmentSetIdentifier;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BalanceSegmentSetEvent that = (BalanceSegmentSetEvent) o;
+ return Objects.equals(productIdentifier, that.productIdentifier) &&
+ Objects.equals(balanceSegmentSetIdentifier, that.balanceSegmentSetIdentifier);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(productIdentifier, balanceSegmentSetIdentifier);
+ }
+
+ @Override
+ public String toString() {
+ return "BalanceSegmentSetEvent{" +
+ "productIdentifier='" + productIdentifier + '\'' +
+ ", balanceSegmentSetIdentifier='" + balanceSegmentSetIdentifier + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java b/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java
index 9412d57..4db9c5f 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java
@@ -29,6 +29,9 @@
String PUT_PRODUCT_ENABLE = "put-enable";
String POST_CASE = "post-case";
String PUT_CASE = "put-case";
+ String POST_BALANCE_SEGMENT_SET = "post-balance-segment-set";
+ String PUT_BALANCE_SEGMENT_SET = "put-balance-segment-set";
+ String DELETE_BALANCE_SEGMENT_SET = "delete-balance-segment-set";
String POST_TASK_DEFINITION = "post-task-definition";
String PUT_TASK_DEFINITION = "put-task-definition";
String DELETE_TASK_DEFINITION = "delete-task-definition";
@@ -44,6 +47,9 @@
String SELECTOR_PUT_PRODUCT_ENABLE = SELECTOR_NAME + " = '" + PUT_PRODUCT_ENABLE + "'";
String SELECTOR_POST_CASE = SELECTOR_NAME + " = '" + POST_CASE + "'";
String SELECTOR_PUT_CASE = SELECTOR_NAME + " = '" + PUT_CASE + "'";
+ String SELECTOR_POST_BALANCE_SEGMENT_SET = SELECTOR_NAME + " = '" + POST_BALANCE_SEGMENT_SET + "'";
+ String SELECTOR_PUT_BALANCE_SEGMENT_SET = SELECTOR_NAME + " = '" + PUT_BALANCE_SEGMENT_SET + "'";
+ String SELECTOR_DELETE_BALANCE_SEGMENT_SET = SELECTOR_NAME + " = '" + DELETE_BALANCE_SEGMENT_SET + "'";
String SELECTOR_POST_TASK_DEFINITION = SELECTOR_NAME + " = '" + POST_TASK_DEFINITION + "'";
String SELECTOR_PUT_TASK_DEFINITION = SELECTOR_NAME + " = '" + PUT_TASK_DEFINITION + "'";
String SELECTOR_DELETE_TASK_DEFINITION = SELECTOR_NAME + " = '" + DELETE_TASK_DEFINITION + "'";
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidChargeDefinition.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidChargeDefinition.java
new file mode 100644
index 0000000..fcdf02a
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidChargeDefinition.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.portfolio.api.v1.validation;
+
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+/**
+ * @author Myrle Krantz
+ */
+public class CheckValidChargeDefinition implements ConstraintValidator<ValidChargeDefinition, ChargeDefinition> {
+ @Override
+ public void initialize(ValidChargeDefinition constraintAnnotation) {
+ }
+
+ @SuppressWarnings("RedundantIfStatement")
+ @Override
+ public boolean isValid(ChargeDefinition value, ConstraintValidatorContext context) {
+ if (value.getAmount() == null)
+ return false;
+ if (value.getAmount().scale() > 4)
+ return false;
+ if (value.getAccrueAction() != null && value.getAccrualAccountDesignator() == null)
+ return false;
+ if (value.getAccrueAction() == null && value.getAccrualAccountDesignator() != null)
+ return false;
+ if (value.getChargeMethod() == ChargeDefinition.ChargeMethod.PROPORTIONAL &&
+ value.getProportionalTo() == null)
+ return false;
+ if (value.getChargeMethod() == ChargeDefinition.ChargeMethod.FIXED &&
+ value.getProportionalTo() != null &&
+ value.getForSegmentSet() == null) //Even if the charge is a fixed charge, we need a proportional to for segment sets.
+ return false;
+ if (value.getForSegmentSet() == null &&
+ (value.getFromSegment() != null || value.getToSegment() != null))
+ return false;
+ if (value.getForSegmentSet() != null &&
+ (value.getFromSegment() == null || value.getToSegment() == null))
+ return false;
+
+ return true;
+ }
+}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidSegmentList.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidSegmentList.java
new file mode 100644
index 0000000..96fc98d
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/CheckValidSegmentList.java
@@ -0,0 +1,59 @@
+/*
+ * 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.portfolio.api.v1.domain.BalanceSegmentSet;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.math.BigDecimal;
+
+/**
+ * @author Myrle Krantz
+ */
+public class CheckValidSegmentList implements ConstraintValidator<ValidSegmentList, BalanceSegmentSet> {
+ @Override
+ public void initialize(ValidSegmentList constraintAnnotation) {
+
+ }
+
+ @Override
+ public boolean isValid(BalanceSegmentSet value, ConstraintValidatorContext context) {
+ if (value.getSegments() == null)
+ return false;
+ if (value.getSegmentIdentifiers() == null)
+ return false;
+
+ if (value.getSegments().size() == 0)
+ return false;
+
+ if (value.getSegments().size() != value.getSegmentIdentifiers().size())
+ return false;
+
+ if (value.getSegments().get(0).compareTo(BigDecimal.ZERO) != 0)
+ return false;
+
+ for (int i = 0; i < value.getSegments().size() -1; i++) {
+ final BigDecimal segment1 = value.getSegments().get(i);
+ final BigDecimal segment2 = value.getSegments().get(i+1);
+
+ if (segment1.compareTo(segment2) > 0)
+ return false;
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidChargeDefinition.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidChargeDefinition.java
new file mode 100644
index 0000000..680fe28
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidChargeDefinition.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.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+ validatedBy = {CheckValidChargeDefinition.class}
+)
+public @interface ValidChargeDefinition {
+ String message() default "Invalid charge definition.";
+
+ Class<?>[] groups() default {};
+
+ Class<? extends Payload>[] payload() default {};
+}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidSegmentList.java b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidSegmentList.java
new file mode 100644
index 0000000..15269ec
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/validation/ValidSegmentList.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.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+ validatedBy = {CheckValidSegmentList.class}
+)
+public @interface ValidSegmentList {
+ String message() default "Segments should be greater than 0 and should number the same as the identifiers.";
+
+ Class<?>[] groups() default {};
+
+ Class<? extends Payload>[] payload() default {};
+}
diff --git a/api/src/test/java/io/mifos/portfolio/api/v1/domain/BalanceSegmentSetTest.java b/api/src/test/java/io/mifos/portfolio/api/v1/domain/BalanceSegmentSetTest.java
new file mode 100644
index 0000000..d787a96
--- /dev/null
+++ b/api/src/test/java/io/mifos/portfolio/api/v1/domain/BalanceSegmentSetTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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 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.*;
+
+/**
+ * @author Myrle Krantz
+ */
+public class BalanceSegmentSetTest extends ValidationTest<BalanceSegmentSet> {
+ public BalanceSegmentSetTest(ValidationTestCase<BalanceSegmentSet> testCase) {
+ super(testCase);
+ }
+
+ @Override
+ protected BalanceSegmentSet createValidTestSubject() {
+ final BalanceSegmentSet ret = new BalanceSegmentSet();
+ ret.setIdentifier("valid");
+ ret.setSegments(Arrays.asList(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.valueOf(10_000)));
+ ret.setSegmentIdentifiers(Arrays.asList("small", "medium", "large"));
+ return ret;
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<ValidationTestCase> ret = new ArrayList<>();
+ ret.add(new ValidationTestCase<BalanceSegmentSet>("basicCase")
+ .adjustment(x -> {})
+ .valid(true));
+ ret.add(new ValidationTestCase<BalanceSegmentSet>("null segments")
+ .adjustment(x -> x.setSegments(null))
+ .valid(false));
+ ret.add(new ValidationTestCase<BalanceSegmentSet>("null identifiers")
+ .adjustment(x -> x.setSegmentIdentifiers(null))
+ .valid(false));
+ ret.add(new ValidationTestCase<BalanceSegmentSet>("too short identifier list")
+ .adjustment(x -> x.setSegmentIdentifiers(Arrays.asList("small", "large")))
+ .valid(false));
+ ret.add(new ValidationTestCase<BalanceSegmentSet>("too short segment list")
+ .adjustment(x -> x.setSegments(Arrays.asList(BigDecimal.ZERO, BigDecimal.valueOf(100))))
+ .valid(false));
+ ret.add(new ValidationTestCase<BalanceSegmentSet>("non-zero first entry")
+ .adjustment(x -> x.setSegments(Arrays.asList(BigDecimal.ONE, BigDecimal.valueOf(100), BigDecimal.valueOf(10_000))))
+ .valid(false));
+ ret.add(new ValidationTestCase<BalanceSegmentSet>("mis-ordered segmentation")
+ .adjustment(x -> x.setSegments(Arrays.asList(BigDecimal.ZERO, BigDecimal.valueOf(10_000), BigDecimal.valueOf(100))))
+ .valid(false));
+ ret.add(new ValidationTestCase<BalanceSegmentSet>("invalid identifier")
+ .adjustment(x -> x.setIdentifier("//"))
+ .valid(false));
+ ret.add(new ValidationTestCase<BalanceSegmentSet>("invalid segment identifier")
+ .adjustment(x -> x.setSegmentIdentifiers(Arrays.asList("small", "large", "//")))
+ .valid(false));
+ return ret;
+ }
+}
\ No newline at end of file
diff --git a/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java b/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java
index 750be0b..8f9ad7e 100644
--- a/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java
+++ b/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java
@@ -98,17 +98,47 @@
.adjustment(x -> x.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED))
.valid(false));
ret.add(new ValidationTestCase<ChargeDefinition>("missingProportionalToIdentifierOnFixedCharge")
- .adjustment(x -> {
- x.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
- x.setProportionalTo(null);
- })
- .valid(true));
+ .adjustment(x -> {
+ x.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+ x.setProportionalTo(null);
+ })
+ .valid(true));
+ ret.add(new ValidationTestCase<ChargeDefinition>("fixed charge proportional so that segment set can be set.")
+ .adjustment(x -> {
+ x.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+ x.setProportionalTo("something");
+ x.setForSegmentSet("xyz");
+ x.setFromSegment("abc");
+ x.setToSegment("def");
+ })
+ .valid(true));
ret.add(new ValidationTestCase<ChargeDefinition>("proportionalToRunningBalance")
.adjustment(x -> x.setProportionalTo("{runningbalance}"))
.valid(true));
ret.add(new ValidationTestCase<ChargeDefinition>("proportionalToMaximumBalance")
.adjustment(x -> x.setProportionalTo("{maximumbalance}"))
.valid(true));
+ ret.add(new ValidationTestCase<ChargeDefinition>("segment set set but not list")
+ .adjustment(x -> x.setForSegmentSet("xyz"))
+ .valid(false));
+ ret.add(new ValidationTestCase<ChargeDefinition>("from segment set but not set")
+ .adjustment(x -> x.setFromSegment("abc"))
+ .valid(false));
+ ret.add(new ValidationTestCase<ChargeDefinition>("to segment set but not set")
+ .adjustment(x -> x.setFromSegment("def"))
+ .valid(false));
+ ret.add(new ValidationTestCase<ChargeDefinition>("valid segment references")
+ .adjustment(x -> { x.setForSegmentSet("xyz"); x.setFromSegment("abc"); x.setToSegment("def");})
+ .valid(true));
+ ret.add(new ValidationTestCase<ChargeDefinition>("invalid segment set identifier")
+ .adjustment(x -> { x.setForSegmentSet("//"); x.setFromSegment("abc"); x.setToSegment("def");})
+ .valid(false));
+ ret.add(new ValidationTestCase<ChargeDefinition>("invalid from segment identifier")
+ .adjustment(x -> { x.setForSegmentSet("xyz"); x.setFromSegment("//"); x.setToSegment("def");})
+ .valid(false));
+ ret.add(new ValidationTestCase<ChargeDefinition>("invalid to segment identifier")
+ .adjustment(x -> { x.setForSegmentSet("xyz"); x.setFromSegment("abc"); x.setToSegment("//");})
+ .valid(false));
return ret;
}
}
diff --git a/component-test/src/main/java/io/mifos/portfolio/Fixture.java b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
index f533a12..dc3cdfe 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -52,7 +52,7 @@
product.setName("Agricultural Loan");
product.setDescription("Loan for seeds or agricultural equipment");
product.setTermRange(new TermRange(ChronoUnit.MONTHS, 12));
- product.setBalanceRange(new BalanceRange(fixScale(BigDecimal.ZERO), fixScale(new BigDecimal(10000))));
+ product.setBalanceRange(new BalanceRange(fixScale(BigDecimal.ZERO), fixScale(new BigDecimal(10_000))));
product.setInterestRange(new InterestRange(BigDecimal.valueOf(3_00, 2), BigDecimal.valueOf(12_00, 2)));
product.setInterestBasis(InterestBasis.CURRENT_BALANCE);
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
index 4ab454a..c4a9e5f 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -24,13 +24,13 @@
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.product.ChargeIdentifiers;
+import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
-import io.mifos.portfolio.api.v1.domain.Case;
-import io.mifos.portfolio.api.v1.domain.CostComponent;
-import io.mifos.portfolio.api.v1.domain.Product;
-import io.mifos.portfolio.api.v1.domain.TaskDefinition;
+import io.mifos.portfolio.api.v1.domain.*;
+import io.mifos.portfolio.api.v1.events.BalanceSegmentSetEvent;
+import io.mifos.portfolio.api.v1.events.ChargeDefinitionEvent;
import io.mifos.portfolio.api.v1.events.EventConstants;
import io.mifos.rhythm.spi.v1.client.BeatListener;
import io.mifos.rhythm.spi.v1.domain.BeatPublish;
@@ -54,9 +54,14 @@
* @author Myrle Krantz
*/
public class TestAccountingInteractionInLoanWorkflow extends AbstractPortfolioTest {
- private static final BigDecimal PROCESSING_FEE_AMOUNT = BigDecimal.valueOf(10_0000, MINOR_CURRENCY_UNIT_DIGITS);
- private static final BigDecimal LOAN_ORIGINATION_FEE_AMOUNT = BigDecimal.valueOf(100_0000, MINOR_CURRENCY_UNIT_DIGITS);
- private static final BigDecimal DISBURSEMENT_FEE_AMOUNT = BigDecimal.valueOf(1_0000, MINOR_CURRENCY_UNIT_DIGITS);
+ private static final BigDecimal PROCESSING_FEE_AMOUNT = BigDecimal.valueOf(100_00, MINOR_CURRENCY_UNIT_DIGITS);
+ private static final BigDecimal LOAN_ORIGINATION_FEE_AMOUNT = BigDecimal.valueOf(100_00, MINOR_CURRENCY_UNIT_DIGITS);
+ private static final BigDecimal DISBURSEMENT_FEE_LOWER_RANGE_AMOUNT = BigDecimal.valueOf(10_00, MINOR_CURRENCY_UNIT_DIGITS);
+ private static final BigDecimal DISBURSEMENT_FEE_UPPER_RANGE_AMOUNT = BigDecimal.valueOf(1_00, MINOR_CURRENCY_UNIT_DIGITS);
+ private static final String DISBURSEMENT_RANGES = "disbursement_ranges";
+ private static final String DISBURSEMENT_LOWER_RANGE = "smaller";
+ private static final String DISBURSEMENT_UPPER_RANGE = "larger";
+ private static final String UPPER_RANGE_DISBURSEMENT_FEE_ID = ChargeIdentifiers.DISBURSEMENT_FEE_ID + "2";
private BeatListener portfolioBeatListener;
@@ -90,7 +95,26 @@
step2CreateCase();
step3OpenCase();
step4ApproveCase();
- step5Disburse(BigDecimal.valueOf(2000L).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
+ step5Disburse(
+ BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
+ UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
+ step6CalculateInterestAccrual();
+ step7PaybackPartialAmount(expectedCurrentBalance);
+ step8Close();
+ }
+
+ @Test
+ public void workflowWithTwoUnequalDisbursals() throws InterruptedException {
+ step1CreateProduct();
+ step2CreateCase();
+ step3OpenCase();
+ step4ApproveCase();
+ step5Disburse(
+ BigDecimal.valueOf(500_00, MINOR_CURRENCY_UNIT_DIGITS),
+ ChargeIdentifiers.DISBURSEMENT_FEE_ID, BigDecimal.valueOf(10_00, MINOR_CURRENCY_UNIT_DIGITS));
+ step5Disburse(
+ BigDecimal.valueOf(1_500_00, MINOR_CURRENCY_UNIT_DIGITS),
+ UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(15_00, MINOR_CURRENCY_UNIT_DIGITS));
step6CalculateInterestAccrual();
step7PaybackPartialAmount(expectedCurrentBalance);
step8Close();
@@ -102,7 +126,9 @@
step2CreateCase();
step3OpenCase();
step4ApproveCase();
- step5Disburse(BigDecimal.valueOf(2000L).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
+ step5Disburse(
+ BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
+ UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
step6CalculateInterestAccrual();
final BigDecimal repayment1 = expectedCurrentBalance.divide(BigDecimal.valueOf(2), BigDecimal.ROUND_HALF_EVEN);
step7PaybackPartialAmount(repayment1.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
@@ -110,13 +136,18 @@
step8Close();
}
- @Test(expected = IllegalArgumentException.class)
+ @Test
public void workflowWithNegativePaymentSize() throws InterruptedException {
step1CreateProduct();
step2CreateCase();
step3OpenCase();
step4ApproveCase();
- step5Disburse(BigDecimal.valueOf(-2).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
+ try {
+ step5Disburse(BigDecimal.valueOf(-2).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN),
+ UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN));
+ Assert.fail("Expected an IllegalArgumentException.");
+ }
+ catch (IllegalArgumentException ignored) { }
}
//Create product and set charges to fixed fees.
@@ -124,9 +155,48 @@
logger.info("step1CreateProduct");
product = createProduct();
+ final BalanceSegmentSet balanceSegmentSet = new BalanceSegmentSet();
+ balanceSegmentSet.setIdentifier(DISBURSEMENT_RANGES);
+ balanceSegmentSet.setSegmentIdentifiers(Arrays.asList(DISBURSEMENT_LOWER_RANGE, DISBURSEMENT_UPPER_RANGE));
+ balanceSegmentSet.setSegments(Arrays.asList(BigDecimal.ZERO, BigDecimal.valueOf(1_000_0000, 4)));
+ portfolioManager.createBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet);
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_BALANCE_SEGMENT_SET, new BalanceSegmentSetEvent(product.getIdentifier(), balanceSegmentSet.getIdentifier())));
+
setFeeToFixedValue(product.getIdentifier(), ChargeIdentifiers.PROCESSING_FEE_ID, PROCESSING_FEE_AMOUNT);
setFeeToFixedValue(product.getIdentifier(), ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, LOAN_ORIGINATION_FEE_AMOUNT);
- setFeeToFixedValue(product.getIdentifier(), ChargeIdentifiers.DISBURSEMENT_FEE_ID, DISBURSEMENT_FEE_AMOUNT);
+
+ final ChargeDefinition lowerRangeDisbursementFeeChargeDefinition
+ = 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.setForSegmentSet(DISBURSEMENT_RANGES);
+ lowerRangeDisbursementFeeChargeDefinition.setFromSegment(DISBURSEMENT_LOWER_RANGE);
+ lowerRangeDisbursementFeeChargeDefinition.setToSegment(DISBURSEMENT_LOWER_RANGE);
+
+ portfolioManager.changeChargeDefinition(product.getIdentifier(), ChargeIdentifiers.DISBURSEMENT_FEE_ID, lowerRangeDisbursementFeeChargeDefinition);
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CHARGE_DEFINITION,
+ new ChargeDefinitionEvent(product.getIdentifier(), ChargeIdentifiers.DISBURSEMENT_FEE_ID)));
+
+ final ChargeDefinition upperRangeDisbursementFeeChargeDefinition = new ChargeDefinition();
+ upperRangeDisbursementFeeChargeDefinition.setIdentifier(UPPER_RANGE_DISBURSEMENT_FEE_ID);
+ upperRangeDisbursementFeeChargeDefinition.setName(UPPER_RANGE_DISBURSEMENT_FEE_ID);
+ upperRangeDisbursementFeeChargeDefinition.setDescription(lowerRangeDisbursementFeeChargeDefinition.getDescription());
+ upperRangeDisbursementFeeChargeDefinition.setFromAccountDesignator(lowerRangeDisbursementFeeChargeDefinition.getFromAccountDesignator());
+ upperRangeDisbursementFeeChargeDefinition.setToAccountDesignator(lowerRangeDisbursementFeeChargeDefinition.getToAccountDesignator());
+ upperRangeDisbursementFeeChargeDefinition.setAccrualAccountDesignator(lowerRangeDisbursementFeeChargeDefinition.getAccrualAccountDesignator());
+ upperRangeDisbursementFeeChargeDefinition.setAccrueAction(lowerRangeDisbursementFeeChargeDefinition.getAccrueAction());
+ upperRangeDisbursementFeeChargeDefinition.setChargeAction(lowerRangeDisbursementFeeChargeDefinition.getChargeAction());
+ upperRangeDisbursementFeeChargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ upperRangeDisbursementFeeChargeDefinition.setAmount(DISBURSEMENT_FEE_UPPER_RANGE_AMOUNT);
+ upperRangeDisbursementFeeChargeDefinition.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
+ upperRangeDisbursementFeeChargeDefinition.setForSegmentSet(DISBURSEMENT_RANGES);
+ upperRangeDisbursementFeeChargeDefinition.setFromSegment(DISBURSEMENT_UPPER_RANGE);
+ upperRangeDisbursementFeeChargeDefinition.setToSegment(DISBURSEMENT_UPPER_RANGE);
+
+ portfolioManager.createChargeDefinition(product.getIdentifier(), upperRangeDisbursementFeeChargeDefinition);
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_CHARGE_DEFINITION,
+ new ChargeDefinitionEvent(product.getIdentifier(), UPPER_RANGE_DISBURSEMENT_FEE_ID)));
taskDefinition = createTaskDefinition(product);
@@ -230,14 +300,17 @@
}
//Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
- private void step5Disburse(final BigDecimal amount) throws InterruptedException {
+ private void step5Disburse(
+ final BigDecimal amount,
+ final String whichDisbursementFee,
+ final BigDecimal disbursementFeeAmount) throws InterruptedException {
logger.info("step5Disburse");
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE,
Collections.singleton(AccountDesignators.ENTRY),
- amount, new CostComponent(ChargeIdentifiers.DISBURSEMENT_FEE_ID, DISBURSEMENT_FEE_AMOUNT),
+ amount, new CostComponent(whichDisbursementFee, disbursementFeeAmount),
new CostComponent(ChargeIdentifiers.DISBURSE_PAYMENT_ID, amount));
checkStateTransfer(
product.getIdentifier(),
@@ -254,12 +327,12 @@
final Set<Debtor> debtors = new HashSet<>();
debtors.add(new Debtor(pendingDisbursalAccountIdentifier, amount.toPlainString()));
debtors.add(new Debtor(AccountingFixture.LOANS_PAYABLE_ACCOUNT_IDENTIFIER, amount.toPlainString()));
- debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, DISBURSEMENT_FEE_AMOUNT.toPlainString()));
+ debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, disbursementFeeAmount.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
creditors.add(new Creditor(customerLoanAccountIdentifier, amount.toPlainString()));
creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, amount.toPlainString()));
- creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, DISBURSEMENT_FEE_AMOUNT.toPlainString()));
+ creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, disbursementFeeAmount.toPlainString()));
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE);
expectedCurrentBalance = expectedCurrentBalance.add(amount);
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestBalanceSegmentSets.java b/component-test/src/main/java/io/mifos/portfolio/TestBalanceSegmentSets.java
new file mode 100644
index 0000000..197b3d5
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/portfolio/TestBalanceSegmentSets.java
@@ -0,0 +1,76 @@
+/*
+ * 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.portfolio.api.v1.client.ProductInUseException;
+import io.mifos.portfolio.api.v1.domain.BalanceSegmentSet;
+import io.mifos.portfolio.api.v1.domain.Product;
+import io.mifos.portfolio.api.v1.events.BalanceSegmentSetEvent;
+import io.mifos.portfolio.api.v1.events.EventConstants;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestBalanceSegmentSets extends AbstractPortfolioTest {
+ @Test
+ public void testBalanceSegmentSetManagement() throws InterruptedException {
+ final Product product = createProduct();
+
+ final BalanceSegmentSet balanceSegmentSet = new BalanceSegmentSet();
+ balanceSegmentSet.setIdentifier(testEnvironment.generateUniqueIdentifer("bss"));
+ balanceSegmentSet.setSegments(Arrays.asList(
+ BigDecimal.ZERO.setScale(4, BigDecimal.ROUND_HALF_EVEN),
+ BigDecimal.TEN.setScale(4, BigDecimal.ROUND_HALF_EVEN),
+ BigDecimal.valueOf(10_000_0000, 4)));
+ balanceSegmentSet.setSegmentIdentifiers(Arrays.asList("abc", "def", "ghi"));
+
+ portfolioManager.createBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet);
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_BALANCE_SEGMENT_SET, new BalanceSegmentSetEvent(product.getIdentifier(), balanceSegmentSet.getIdentifier())));
+
+ final BalanceSegmentSet createdBalanceSegmentSet = portfolioManager.getBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet.getIdentifier());
+ Assert.assertEquals(balanceSegmentSet, createdBalanceSegmentSet);
+
+ balanceSegmentSet.setSegments(Arrays.asList(
+ BigDecimal.ZERO.setScale(4, BigDecimal.ROUND_HALF_EVEN),
+ BigDecimal.valueOf(100_0000, 4),
+ BigDecimal.valueOf(10_000_0000, 4)));
+ balanceSegmentSet.setSegmentIdentifiers(Arrays.asList("abc", "def", "ghi"));
+
+ portfolioManager.changeBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet.getIdentifier(), balanceSegmentSet);
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_BALANCE_SEGMENT_SET, new BalanceSegmentSetEvent(product.getIdentifier(), balanceSegmentSet.getIdentifier())));
+
+ final BalanceSegmentSet changedBalanceSegmentSet = portfolioManager.getBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet.getIdentifier());
+ Assert.assertEquals(balanceSegmentSet, changedBalanceSegmentSet);
+
+ portfolioManager.deleteBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet.getIdentifier());
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.DELETE_BALANCE_SEGMENT_SET, new BalanceSegmentSetEvent(product.getIdentifier(), balanceSegmentSet.getIdentifier())));
+
+ enableProduct(product);
+
+ createCase(product.getIdentifier());
+
+ try {
+ portfolioManager.createBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet);
+ Assert.fail("shouldn't be able to create a balance segment set in a product in use.");
+ }
+ catch (final ProductInUseException ignored) { }
+ }
+}
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 e7bd386..44d8cc3 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestSuite.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestSuite.java
@@ -24,6 +24,7 @@
@RunWith(Suite.class)
@Suite.SuiteClasses({
TestAccountingInteractionInLoanWorkflow.class,
+ TestBalanceSegmentSets.class,
TestCases.class,
TestChargeDefinitions.class,
TestCommands.class,
diff --git a/component-test/src/main/java/io/mifos/portfolio/listener/BalanceSegmentSetEventListener.java b/component-test/src/main/java/io/mifos/portfolio/listener/BalanceSegmentSetEventListener.java
new file mode 100644
index 0000000..528fc7c
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/portfolio/listener/BalanceSegmentSetEventListener.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.portfolio.listener;
+
+import io.mifos.core.lang.config.TenantHeaderFilter;
+import io.mifos.core.test.listener.EventRecorder;
+import io.mifos.portfolio.api.v1.events.BalanceSegmentSetEvent;
+import io.mifos.portfolio.api.v1.events.EventConstants;
+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
+ */
+@Component
+public class BalanceSegmentSetEventListener {
+ private final EventRecorder eventRecorder;
+
+ @SuppressWarnings("SpringJavaAutowiringInspection")
+ @Autowired
+ public BalanceSegmentSetEventListener(final EventRecorder eventRecorder) {
+ super();
+ this.eventRecorder = eventRecorder;
+ }
+
+ @JmsListener(
+ subscription = EventConstants.DESTINATION,
+ destination = EventConstants.DESTINATION,
+ selector = EventConstants.SELECTOR_POST_BALANCE_SEGMENT_SET
+ )
+ public void onCreateBalanceSegmentSet(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+ final String payload) {
+ this.eventRecorder.event(tenant, EventConstants.POST_BALANCE_SEGMENT_SET, payload, BalanceSegmentSetEvent.class);
+ }
+
+ @JmsListener(
+ subscription = EventConstants.DESTINATION,
+ destination = EventConstants.DESTINATION,
+ selector = EventConstants.SELECTOR_PUT_BALANCE_SEGMENT_SET
+ )
+ public void onChangeBalanceSegmentSet(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+ final String payload) {
+ this.eventRecorder.event(tenant, EventConstants.PUT_BALANCE_SEGMENT_SET, payload, BalanceSegmentSetEvent.class);
+ }
+
+ @JmsListener(
+ subscription = EventConstants.DESTINATION,
+ destination = EventConstants.DESTINATION,
+ selector = EventConstants.SELECTOR_DELETE_BALANCE_SEGMENT_SET
+ )
+ public void onDeleteBalanceSegmentSet(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+ final String payload) {
+ this.eventRecorder.event(tenant, EventConstants.DELETE_BALANCE_SEGMENT_SET, payload, BalanceSegmentSetEvent.class);
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ChargeRange.java b/service/src/main/java/io/mifos/individuallending/internal/service/ChargeRange.java
new file mode 100644
index 0000000..cd293d5
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ChargeRange.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+class ChargeRange {
+ final private BigDecimal from;
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ final private Optional<BigDecimal> to;
+
+ ChargeRange(
+ final BigDecimal from,
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<BigDecimal> to) {
+ this.from = from;
+ this.to = to;
+ }
+
+ boolean amountIsWithinRange(BigDecimal amountProportionalTo) {
+ return to.map(bigDecimal -> from.compareTo(amountProportionalTo) <= 0 &&
+ bigDecimal.compareTo(amountProportionalTo) > 0)
+ .orElseGet(() -> from.compareTo(amountProportionalTo) <= 0);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ChargeRange that = (ChargeRange) o;
+ return Objects.equals(from, that.from) &&
+ Objects.equals(to, that.to);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(from, to);
+ }
+
+ @Override
+ public String toString() {
+ return "ChargeRange{" +
+ "from=" + from +
+ ", to=" + to +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
index 318db3b..c3ba820 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
@@ -47,14 +47,14 @@
private static final int EXTRA_PRECISION = 4;
private static final int RUNNING_CALCULATION_PRECISION = 8;
- private final IndividualLoanService individualLoanService;
+ private final ScheduledChargesService scheduledChargesService;
private final AccountingAdapter accountingAdapter;
@Autowired
public CostComponentService(
- final IndividualLoanService individualLoanService,
- final AccountingAdapter accountingAdapter) {
- this.individualLoanService = individualLoanService;
+ final ScheduledChargesService scheduledChargesService,
+ final AccountingAdapter accountingAdapter) {
+ this.scheduledChargesService = scheduledChargesService;
this.accountingAdapter = accountingAdapter;
}
@@ -93,7 +93,7 @@
final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.OPEN, today()));
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier, scheduledActions);
return getCostComponentsForScheduledCharges(
@@ -112,7 +112,7 @@
final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.DENY, today()));
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier, scheduledActions);
return getCostComponentsForScheduledCharges(
@@ -132,7 +132,7 @@
final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.APPROVE, today()));
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier, scheduledActions);
return getCostComponentsForScheduledCharges(
@@ -173,7 +173,7 @@
else
disbursalSize = requestedDisbursalSize.negate();
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier, scheduledActions);
@@ -219,7 +219,7 @@
final LocalDate today = today();
final ScheduledAction interestAction = new ScheduledAction(Action.APPLY_INTEREST, today, new Period(1, today));
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier,
Collections.singletonList(interestAction));
@@ -271,7 +271,7 @@
final List<ScheduledAction> hypotheticalScheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(
today(),
caseParameters);
- final List<ScheduledCharge> hypotheticalScheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> hypotheticalScheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier,
hypotheticalScheduledActions);
loanPaymentSize = getLoanPaymentSize(
@@ -281,7 +281,7 @@
hypotheticalScheduledCharges);
}
- final List<ScheduledCharge> scheduledChargesForThisAction = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledChargesForThisAction = scheduledChargesService.getScheduledCharges(
productIdentifier,
Collections.singletonList(scheduledAction));
@@ -307,12 +307,12 @@
true);
}
- public static boolean isAccruedChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
+ private static boolean isAccruedChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
return chargeDefinition.getAccrueAction() != null &&
chargeDefinition.getChargeAction().equals(action.name());
}
- public static boolean isAccrualChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
+ private static boolean isAccrualChargeForAction(final ChargeDefinition chargeDefinition, final Action action) {
return chargeDefinition.getAccrueAction() != null &&
chargeDefinition.getAccrueAction().equals(action.name());
}
@@ -367,7 +367,7 @@
final LocalDate today = today();
final ScheduledAction closeAction = new ScheduledAction(Action.CLOSE, today, new Period(1, today));
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier,
Collections.singletonList(closeAction));
@@ -440,6 +440,9 @@
entryAccountAdjustment,
balanceAdjustments);
//TODO: getAmountProportionalTo is programmed under the assumption of non-accrual accounting.
+ if (scheduledCharge.getChargeRange().map(x ->
+ !x.amountIsWithinRange(amountProportionalTo)).orElse(false))
+ continue;
final CostComponent costComponent = costComponentMap
.computeIfAbsent(scheduledCharge.getChargeDefinition(), CostComponentService::constructEmptyCostComponent);
@@ -468,27 +471,37 @@
final BigDecimal runningBalance,
final BigDecimal loanPaymentSize,
final Map<String, BigDecimal> balanceAdjustments) {
- final Optional<ChargeProportionalDesignator> optionalChargeProportionalTo = proportionalToDesignator(scheduledCharge);
- return optionalChargeProportionalTo.map(chargeProportionalTo -> {
- switch (chargeProportionalTo) {
- case NOT_PROPORTIONAL:
- return BigDecimal.ZERO;
- case MAXIMUM_BALANCE_DESIGNATOR:
- return maximumBalance;
- case RUNNING_BALANCE_DESIGNATOR:
- return runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
- case REPAYMENT_DESIGNATOR:
- return loanPaymentSize;
- case PRINCIPAL_ADJUSTMENT_DESIGNATOR: {
- final BigDecimal newRunningBalance
- = runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
- final BigDecimal newLoanPaymentSize = loanPaymentSize.min(newRunningBalance);
- return newLoanPaymentSize.add(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO)).abs();
- }
- default:
- return BigDecimal.ZERO;
+ final Optional<ChargeProportionalDesignator> optionalChargeProportionalTo
+ = ChargeProportionalDesignator.fromString(scheduledCharge.getChargeDefinition().getProportionalTo());
+ return optionalChargeProportionalTo.map(chargeProportionalTo ->
+ getAmountProportionalTo(chargeProportionalTo, maximumBalance, runningBalance, loanPaymentSize, balanceAdjustments))
+ .orElse(BigDecimal.ZERO);
+ }
+
+ static BigDecimal getAmountProportionalTo(
+ final ChargeProportionalDesignator chargeProportionalTo,
+ final BigDecimal maximumBalance,
+ final BigDecimal runningBalance,
+ final BigDecimal loanPaymentSize,
+ final Map<String, BigDecimal> balanceAdjustments) {
+ switch (chargeProportionalTo) {
+ case NOT_PROPORTIONAL:
+ return BigDecimal.ZERO;
+ case MAXIMUM_BALANCE_DESIGNATOR:
+ return maximumBalance;
+ case RUNNING_BALANCE_DESIGNATOR:
+ return runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
+ case REPAYMENT_DESIGNATOR:
+ return loanPaymentSize;
+ case PRINCIPAL_ADJUSTMENT_DESIGNATOR: {
+ final BigDecimal newRunningBalance
+ = runningBalance.subtract(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO));
+ final BigDecimal newLoanPaymentSize = loanPaymentSize.min(newRunningBalance);
+ return newLoanPaymentSize.add(balanceAdjustments.getOrDefault(AccountDesignators.CUSTOMER_LOAN, BigDecimal.ZERO)).abs();
}
- }).orElse(BigDecimal.ZERO);
+ default:
+ return BigDecimal.ZERO;
+ }
//TODO: correctly implement charges which are proportional to other charges.
}
@@ -499,14 +512,6 @@
return ret;
}
- private static Optional<ChargeProportionalDesignator> proportionalToDesignator(final ScheduledCharge scheduledCharge) {
- if (!scheduledCharge.getChargeDefinition().getChargeMethod().equals(ChargeDefinition.ChargeMethod.PROPORTIONAL) &&
- !scheduledCharge.getChargeDefinition().getChargeMethod().equals(ChargeDefinition.ChargeMethod.INTEREST))
- return Optional.of(ChargeProportionalDesignator.NOT_PROPORTIONAL);
-
- return ChargeProportionalDesignator.fromString(scheduledCharge.getChargeDefinition().getProportionalTo());
- }
-
private static Function<BigDecimal, BigDecimal> howToApplyScheduledChargeToAmount(
final ScheduledCharge scheduledCharge, final BigDecimal interest)
{
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java b/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
index 6320deb..e836004 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
@@ -18,9 +18,6 @@
import io.mifos.individuallending.api.v1.domain.caseinstance.ChargeName;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
-import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
-import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -30,18 +27,17 @@
import java.util.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* @author Myrle Krantz
*/
@Service
public class IndividualLoanService {
- private final ChargeDefinitionService chargeDefinitionService;
+ private final ScheduledChargesService scheduledChargesService;
@Autowired
- public IndividualLoanService(final ChargeDefinitionService chargeDefinitionService) {
- this.chargeDefinitionService = chargeDefinitionService;
+ public IndividualLoanService(final ScheduledChargesService scheduledChargesService) {
+ this.scheduledChargesService = scheduledChargesService;
}
public PlannedPaymentPage getPlannedPaymentsPage(
@@ -53,7 +49,7 @@
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(initialDisbursalDate, dataContextOfAction.getCaseParameters());
- final List<ScheduledCharge> scheduledCharges = getScheduledCharges(dataContextOfAction.getProduct().getIdentifier(), scheduledActions);
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(dataContextOfAction.getProduct().getIdentifier(), scheduledActions);
final BigDecimal loanPaymentSize = CostComponentService.getLoanPaymentSize(
dataContextOfAction.getCaseParameters().getMaximumBalance(),
@@ -98,21 +94,6 @@
return new ChargeName(scheduledCharge.getChargeDefinition().getIdentifier(), scheduledCharge.getChargeDefinition().getName());
}
- List<ScheduledCharge> getScheduledCharges(
- final String productIdentifier,
- final @Nonnull List<ScheduledAction> scheduledActions) {
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction
- = chargeDefinitionService.getChargeDefinitionsMappedByChargeAction(productIdentifier);
-
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction
- = chargeDefinitionService.getChargeDefinitionsMappedByAccrueAction(productIdentifier);
-
- return getScheduledCharges(
- scheduledActions,
- chargeDefinitionsMappedByChargeAction,
- chargeDefinitionsMappedByAccrueAction);
- }
-
static private List<PlannedPayment> getPlannedPaymentsElements(
final BigDecimal initialBalance,
final int minorCurrencyUnitDigits,
@@ -124,7 +105,7 @@
.collect(Collectors.groupingBy(IndividualLoanService::getPeriodFromScheduledCharge,
Collectors.mapping(x -> x,
Collector.of(
- () -> new TreeSet<>(new ScheduledChargeComparator()),
+ () -> new TreeSet<>(new ScheduledChargesService.ScheduledChargeComparator()),
SortedSet::add,
(left, right) -> { left.addAll(right); return left; }))));
@@ -179,72 +160,4 @@
else
return scheduledAction.repaymentPeriod;
}
-
- static List<ScheduledCharge> getScheduledCharges(final List<ScheduledAction> scheduledActions,
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction) {
- return scheduledActions.stream()
- .flatMap(scheduledAction ->
- getChargeDefinitionStream(
- chargeDefinitionsMappedByChargeAction,
- chargeDefinitionsMappedByAccrueAction,
- scheduledAction)
- .map(chargeDefinition -> new ScheduledCharge(scheduledAction, chargeDefinition)))
- .collect(Collectors.toList());
- }
-
- private static Stream<ChargeDefinition> getChargeDefinitionStream(
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
- final ScheduledAction scheduledAction) {
- final List<ChargeDefinition> chargeMappingList = chargeDefinitionsMappedByChargeAction
- .get(scheduledAction.action.name());
- Stream<ChargeDefinition> chargeMapping = chargeMappingList == null ? Stream.empty() : chargeMappingList.stream();
- if (chargeMapping == null)
- chargeMapping = Stream.empty();
-
- final List<ChargeDefinition> accrueMappingList = chargeDefinitionsMappedByAccrueAction
- .get(scheduledAction.action.name());
- Stream<ChargeDefinition> accrueMapping = accrueMappingList == null ? Stream.empty() : accrueMappingList.stream();
- if (accrueMapping == null)
- accrueMapping = Stream.empty();
-
- return Stream.concat(
- accrueMapping.sorted(IndividualLoanService::proportionalityApplicationOrder),
- chargeMapping.sorted(IndividualLoanService::proportionalityApplicationOrder));
- }
-
- private static class ScheduledChargeComparator implements Comparator<ScheduledCharge>
- {
- @Override
- public int compare(ScheduledCharge o1, ScheduledCharge o2) {
- int ret = o1.getScheduledAction().when.compareTo(o2.getScheduledAction().when);
- if (ret == 0)
- ret = o1.getScheduledAction().action.compareTo(o2.getScheduledAction().action);
- if (ret == 0)
- ret = proportionalityApplicationOrder(o1.getChargeDefinition(), o2.getChargeDefinition());
- if (ret == 0)
- return o1.getChargeDefinition().getIdentifier().compareTo(o2.getChargeDefinition().getIdentifier());
- else
- return ret;
- }
- }
-
- private static int proportionalityApplicationOrder(final ChargeDefinition o1, final ChargeDefinition o2) {
- final Optional<ChargeProportionalDesignator> aProportionalToDesignator
- = ChargeProportionalDesignator.fromString(o1.getProportionalTo());
- final Optional<ChargeProportionalDesignator> bProportionalToDesignator
- = ChargeProportionalDesignator.fromString(o2.getProportionalTo());
-
- if (aProportionalToDesignator.isPresent() && bProportionalToDesignator.isPresent())
- return Integer.compare(
- aProportionalToDesignator.get().getOrderOfApplication(),
- bProportionalToDesignator.get().getOrderOfApplication());
- else if (aProportionalToDesignator.isPresent())
- return 1;
- else if (bProportionalToDesignator.isPresent())
- return -1;
- else
- return 0;
- }
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java
index 60fb56c..7b98317 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledCharge.java
@@ -18,6 +18,7 @@
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import javax.annotation.Nonnull;
+import java.util.Optional;
/**
* @author Myrle Krantz
@@ -25,10 +26,16 @@
public class ScheduledCharge {
private final ScheduledAction scheduledAction;
private final ChargeDefinition chargeDefinition;
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ private final Optional<ChargeRange> chargeRange;
- ScheduledCharge(@Nonnull final ScheduledAction scheduledAction, @Nonnull final ChargeDefinition chargeDefinition) {
+ ScheduledCharge(
+ @Nonnull final ScheduledAction scheduledAction,
+ @Nonnull final ChargeDefinition chargeDefinition,
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @Nonnull final Optional<ChargeRange> chargeRange) {
this.scheduledAction = scheduledAction;
this.chargeDefinition = chargeDefinition;
+ this.chargeRange = chargeRange;
}
ScheduledAction getScheduledAction() {
@@ -39,11 +46,16 @@
return chargeDefinition;
}
+ Optional<ChargeRange> getChargeRange() {
+ return chargeRange;
+ }
+
@Override
public String toString() {
return "ScheduledCharge{" +
- "scheduledAction=" + scheduledAction +
- ", chargeDefinition=" + chargeDefinition +
- '}';
+ "scheduledAction=" + scheduledAction +
+ ", chargeDefinition=" + chargeDefinition +
+ ", chargeRange=" + chargeRange +
+ '}';
}
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargesService.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargesService.java
new file mode 100644
index 0000000..fcbfc6c
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledChargesService.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentEntity;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
+import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Nonnull;
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class ScheduledChargesService {
+ private final ChargeDefinitionService chargeDefinitionService;
+ private final BalanceSegmentRepository balanceSegmentRepository;
+
+ @Autowired
+ public ScheduledChargesService(
+ final ChargeDefinitionService chargeDefinitionService,
+ final BalanceSegmentRepository balanceSegmentRepository) {
+ this.chargeDefinitionService = chargeDefinitionService;
+ this.balanceSegmentRepository = balanceSegmentRepository;
+ }
+
+ List<ScheduledCharge> getScheduledCharges(
+ final String productIdentifier,
+ final @Nonnull List<ScheduledAction> scheduledActions) {
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction
+ = chargeDefinitionService.getChargeDefinitionsMappedByChargeAction(productIdentifier);
+
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction
+ = chargeDefinitionService.getChargeDefinitionsMappedByAccrueAction(productIdentifier);
+
+ return getScheduledCharges(
+ productIdentifier,
+ scheduledActions,
+ chargeDefinitionsMappedByChargeAction,
+ chargeDefinitionsMappedByAccrueAction);
+ }
+
+ private List<ScheduledCharge> getScheduledCharges(
+ final String productIdentifier,
+ final List<ScheduledAction> scheduledActions,
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction) {
+ return scheduledActions.stream()
+ .flatMap(scheduledAction ->
+ getChargeDefinitionStream(
+ chargeDefinitionsMappedByChargeAction,
+ chargeDefinitionsMappedByAccrueAction,
+ scheduledAction)
+ .map(chargeDefinition -> new ScheduledCharge(
+ scheduledAction,
+ chargeDefinition,
+ findChargeRange(productIdentifier, chargeDefinition))))
+ .collect(Collectors.toList());
+ }
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ private static class Segment {
+ final String identifier;
+ final BigDecimal lowerBound;
+ final Optional<BigDecimal> upperBound;
+
+ private Segment(final String segmentIdentifier,
+ final BigDecimal lowerBound,
+ final Optional<BigDecimal> upperBound) {
+ this.identifier = segmentIdentifier;
+ this.lowerBound = lowerBound;
+ this.upperBound = upperBound;
+ }
+
+ BigDecimal getLowerBound() {
+ return lowerBound;
+ }
+
+ Optional<BigDecimal> getUpperBound() {
+ return upperBound;
+ }
+ }
+
+ Optional<ChargeRange> findChargeRange(final String productIdentifier, final ChargeDefinition chargeDefinition) {
+ if ((chargeDefinition.getForSegmentSet() == null) ||
+ (chargeDefinition.getFromSegment() == null) ||
+ (chargeDefinition.getToSegment() == null))
+ return Optional.empty();
+
+ final List<BalanceSegmentEntity> segmentSet = balanceSegmentRepository.findByProductIdentifierAndSegmentSetIdentifier(productIdentifier, chargeDefinition.getForSegmentSet())
+ .sorted(Comparator.comparing(BalanceSegmentEntity::getLowerBound))
+ .collect(Collectors.toList());
+
+ final Map<String, Segment> segments = Stream.iterate(0, i -> i + 1).limit(segmentSet.size())
+ .map(i -> new Segment(
+ segmentSet.get(i).getSegmentIdentifier(),
+ segmentSet.get(i).getLowerBound(),
+ Optional.ofNullable(i + 1 < segmentSet.size() ?
+ segmentSet.get(i + 1).getLowerBound() :
+ null)
+ ))
+ .collect(Collectors.toMap(x -> x.identifier, x -> x));
+
+
+ final Optional<Segment> fromSegment = Optional.ofNullable(segments.get(chargeDefinition.getFromSegment()));
+ final Optional<Segment> toSegment = Optional.ofNullable(segments.get(chargeDefinition.getToSegment()));
+ if (!fromSegment.isPresent() || !toSegment.isPresent())
+ return Optional.empty();
+
+ return Optional.of(new ChargeRange(fromSegment.get().getLowerBound(), toSegment.get().getUpperBound()));
+ }
+
+ private static Stream<ChargeDefinition> getChargeDefinitionStream(
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
+ final ScheduledAction scheduledAction) {
+ final List<ChargeDefinition> chargeMappingList = chargeDefinitionsMappedByChargeAction
+ .get(scheduledAction.action.name());
+ Stream<ChargeDefinition> chargeMapping = chargeMappingList == null ? Stream.empty() : chargeMappingList.stream();
+ if (chargeMapping == null)
+ chargeMapping = Stream.empty();
+
+ final List<ChargeDefinition> accrueMappingList = chargeDefinitionsMappedByAccrueAction
+ .get(scheduledAction.action.name());
+ Stream<ChargeDefinition> accrueMapping = accrueMappingList == null ? Stream.empty() : accrueMappingList.stream();
+ if (accrueMapping == null)
+ accrueMapping = Stream.empty();
+
+ return Stream.concat(
+ accrueMapping.sorted(ScheduledChargesService::proportionalityApplicationOrder),
+ chargeMapping.sorted(ScheduledChargesService::proportionalityApplicationOrder));
+ }
+
+ static class ScheduledChargeComparator implements Comparator<ScheduledCharge>
+ {
+ @Override
+ public int compare(ScheduledCharge o1, ScheduledCharge o2) {
+ int ret = o1.getScheduledAction().when.compareTo(o2.getScheduledAction().when);
+ if (ret == 0)
+ ret = o1.getScheduledAction().action.compareTo(o2.getScheduledAction().action);
+ if (ret == 0)
+ ret = proportionalityApplicationOrder(o1.getChargeDefinition(), o2.getChargeDefinition());
+ if (ret == 0)
+ return o1.getChargeDefinition().getIdentifier().compareTo(o2.getChargeDefinition().getIdentifier());
+ else
+ return ret;
+ }
+ }
+
+ private static int proportionalityApplicationOrder(final ChargeDefinition o1, final ChargeDefinition o2) {
+ final Optional<ChargeProportionalDesignator> aProportionalToDesignator
+ = ChargeProportionalDesignator.fromString(o1.getProportionalTo());
+ final Optional<ChargeProportionalDesignator> bProportionalToDesignator
+ = ChargeProportionalDesignator.fromString(o2.getProportionalTo());
+
+ if (aProportionalToDesignator.isPresent() && bProportionalToDesignator.isPresent())
+ return Integer.compare(
+ aProportionalToDesignator.get().getOrderOfApplication(),
+ bProportionalToDesignator.get().getOrderOfApplication());
+ else if (aProportionalToDesignator.isPresent())
+ return 1;
+ else if (bProportionalToDesignator.isPresent())
+ return -1;
+ else
+ return 0;
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/ChangeBalanceSegmentSetCommand.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/ChangeBalanceSegmentSetCommand.java
new file mode 100644
index 0000000..19edb4c
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/ChangeBalanceSegmentSetCommand.java
@@ -0,0 +1,63 @@
+/*
+ * 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.command;
+
+import io.mifos.portfolio.api.v1.domain.BalanceSegmentSet;
+
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+public class ChangeBalanceSegmentSetCommand {
+ private final String productIdentifier;
+ private final BalanceSegmentSet instance;
+
+ public ChangeBalanceSegmentSetCommand(String productIdentifier, BalanceSegmentSet instance) {
+ this.productIdentifier = productIdentifier;
+ this.instance = instance;
+ }
+
+ public String getProductIdentifier() {
+ return productIdentifier;
+ }
+
+ public BalanceSegmentSet getInstance() {
+ return instance;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ChangeBalanceSegmentSetCommand that = (ChangeBalanceSegmentSetCommand) o;
+ return Objects.equals(productIdentifier, that.productIdentifier) &&
+ Objects.equals(instance, that.instance);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(productIdentifier, instance);
+ }
+
+ @Override
+ public String toString() {
+ return "ChangeBalanceSegmentSetCommand{" +
+ "productIdentifier='" + productIdentifier + '\'' +
+ ", instance=" + instance +
+ '}';
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/CreateBalanceSegmentSetCommand.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/CreateBalanceSegmentSetCommand.java
new file mode 100644
index 0000000..a5a4925
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/CreateBalanceSegmentSetCommand.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.portfolio.service.internal.command;
+
+import io.mifos.portfolio.api.v1.domain.BalanceSegmentSet;
+
+/**
+ * @author Myrle Krantz
+ */
+public class CreateBalanceSegmentSetCommand {
+ private final String productIdentifier;
+ private final BalanceSegmentSet instance;
+
+ public CreateBalanceSegmentSetCommand(String productIdentifier, BalanceSegmentSet instance) {
+ this.productIdentifier = productIdentifier;
+ this.instance = instance;
+ }
+
+ public String getProductIdentifier() {
+ return productIdentifier;
+ }
+
+ public BalanceSegmentSet getInstance() {
+ return instance;
+ }
+
+ @Override
+ public String toString() {
+ return "CreateBalanceSegmentSetCommand{" +
+ "productIdentifier='" + productIdentifier + '\'' +
+ ", instance=" + instance +
+ '}';
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/DeleteBalanceSegmentSetCommand.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/DeleteBalanceSegmentSetCommand.java
new file mode 100644
index 0000000..cb4ac07
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/DeleteBalanceSegmentSetCommand.java
@@ -0,0 +1,45 @@
+/*
+ * 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.command;
+
+/**
+ * @author Myrle Krantz
+ */
+public class DeleteBalanceSegmentSetCommand {
+ private final String productIdentifier;
+ private final String balanceSegmentSetIdentifier;
+
+ public DeleteBalanceSegmentSetCommand(String productIdentifier, String balanceSegmentSetIdentifier) {
+ this.productIdentifier = productIdentifier;
+ this.balanceSegmentSetIdentifier = balanceSegmentSetIdentifier;
+ }
+
+ public String getProductIdentifier() {
+ return productIdentifier;
+ }
+
+ public String getBalanceSegmentSetIdentifier() {
+ return balanceSegmentSetIdentifier;
+ }
+
+ @Override
+ public String toString() {
+ return "DeleteBalanceSegmentSetCommand{" +
+ "productIdentifier='" + productIdentifier + '\'' +
+ ", balanceSegmentSetIdentifier='" + balanceSegmentSetIdentifier + '\'' +
+ '}';
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/BalanceSegmentSetCommandHandler.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/BalanceSegmentSetCommandHandler.java
new file mode 100644
index 0000000..ae83771
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/BalanceSegmentSetCommandHandler.java
@@ -0,0 +1,119 @@
+/*
+ * 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.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.portfolio.api.v1.events.BalanceSegmentSetEvent;
+import io.mifos.portfolio.api.v1.events.EventConstants;
+import io.mifos.portfolio.service.internal.command.ChangeBalanceSegmentSetCommand;
+import io.mifos.portfolio.service.internal.command.CreateBalanceSegmentSetCommand;
+import io.mifos.portfolio.service.internal.command.DeleteBalanceSegmentSetCommand;
+import io.mifos.portfolio.service.internal.mapper.BalanceSegmentSetMapper;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentEntity;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
+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.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Myrle Krantz
+ */
+@Aggregate
+public class BalanceSegmentSetCommandHandler {
+ private final BalanceSegmentRepository balanceSegmentRepository;
+ private final ProductRepository productRepository;
+
+ @Autowired
+ public BalanceSegmentSetCommandHandler(
+ final BalanceSegmentRepository balanceSegmentRepository,
+ final ProductRepository productRepository) {
+ this.balanceSegmentRepository = balanceSegmentRepository;
+ this.productRepository = productRepository;
+ }
+
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.POST_BALANCE_SEGMENT_SET)
+ public BalanceSegmentSetEvent process(final CreateBalanceSegmentSetCommand createBalanceSegmentSetCommand) {
+ final ProductEntity product = productRepository.findByIdentifier(createBalanceSegmentSetCommand.getProductIdentifier())
+ .orElseThrow(() -> ServiceException.notFound("Product with identifier ''{0}'' doesn''t exist.", createBalanceSegmentSetCommand.getProductIdentifier()));
+
+ final List<BalanceSegmentEntity> balanceSegmentSetEntities = BalanceSegmentSetMapper.map(
+ createBalanceSegmentSetCommand.getInstance(), product);
+
+ balanceSegmentRepository.save(balanceSegmentSetEntities);
+
+ return new BalanceSegmentSetEvent(
+ createBalanceSegmentSetCommand.getProductIdentifier(),
+ createBalanceSegmentSetCommand.getInstance().getIdentifier());
+ }
+
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.PUT_BALANCE_SEGMENT_SET)
+ public BalanceSegmentSetEvent process(final ChangeBalanceSegmentSetCommand changeBalanceSegmentSetCommand) {
+ final ProductEntity product = productRepository.findByIdentifier(changeBalanceSegmentSetCommand.getProductIdentifier())
+ .orElseThrow(() -> ServiceException.notFound("Product with identifier ''{0}'' doesn''t exist.", changeBalanceSegmentSetCommand.getProductIdentifier()));
+
+ final List<BalanceSegmentEntity> balanceSegmentSets = balanceSegmentRepository.findByProductIdentifierAndSegmentSetIdentifier(
+ changeBalanceSegmentSetCommand.getProductIdentifier(),
+ changeBalanceSegmentSetCommand.getInstance().getIdentifier())
+ .collect(Collectors.toList());
+ if (balanceSegmentSets.isEmpty())
+ throw ServiceException.notFound("Segment set with identifier ''{0}.{1}'' doesn''t exist.",
+ changeBalanceSegmentSetCommand.getProductIdentifier(),
+ changeBalanceSegmentSetCommand.getInstance().getIdentifier());
+
+ balanceSegmentRepository.deleteInBatch(balanceSegmentSets);
+
+ final List<BalanceSegmentEntity> balanceSegmentSetEntities = BalanceSegmentSetMapper.map(
+ changeBalanceSegmentSetCommand.getInstance(), product);
+
+ balanceSegmentRepository.save(balanceSegmentSetEntities);
+
+ return new BalanceSegmentSetEvent(
+ changeBalanceSegmentSetCommand.getProductIdentifier(),
+ changeBalanceSegmentSetCommand.getInstance().getIdentifier());
+ }
+
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.DELETE_BALANCE_SEGMENT_SET)
+ public BalanceSegmentSetEvent process(final DeleteBalanceSegmentSetCommand deleteBalanceSegmentSetCommand) {
+ final List<BalanceSegmentEntity> balanceSegmentSets = balanceSegmentRepository.findByProductIdentifierAndSegmentSetIdentifier(
+ deleteBalanceSegmentSetCommand.getProductIdentifier(),
+ deleteBalanceSegmentSetCommand.getBalanceSegmentSetIdentifier())
+ .collect(Collectors.toList());
+ if (balanceSegmentSets.isEmpty())
+ throw ServiceException.notFound("Segment set with identifier ''{0}.{1}'' doesn''t exist.",
+ deleteBalanceSegmentSetCommand.getProductIdentifier(),
+ deleteBalanceSegmentSetCommand.getBalanceSegmentSetIdentifier());
+
+ balanceSegmentRepository.deleteInBatch(balanceSegmentSets);
+
+ return new BalanceSegmentSetEvent(
+ deleteBalanceSegmentSetCommand.getProductIdentifier(),
+ deleteBalanceSegmentSetCommand.getBalanceSegmentSetIdentifier());
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ChargeDefinitionCommandHandler.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ChargeDefinitionCommandHandler.java
index 61b7db9..6cfb5a1 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ChargeDefinitionCommandHandler.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/ChargeDefinitionCommandHandler.java
@@ -15,7 +15,11 @@
*/
package io.mifos.portfolio.service.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.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.events.ChargeDefinitionEvent;
import io.mifos.portfolio.api.v1.events.EventConstants;
@@ -23,17 +27,12 @@
import io.mifos.portfolio.service.internal.command.CreateChargeDefinitionCommand;
import io.mifos.portfolio.service.internal.command.DeleteProductChargeDefinitionCommand;
import io.mifos.portfolio.service.internal.mapper.ChargeDefinitionMapper;
-import io.mifos.portfolio.service.internal.repository.ChargeDefinitionEntity;
-import io.mifos.portfolio.service.internal.repository.ChargeDefinitionRepository;
-import io.mifos.portfolio.service.internal.repository.ProductEntity;
-import io.mifos.portfolio.service.internal.repository.ProductRepository;
-import io.mifos.core.command.annotation.Aggregate;
-import io.mifos.core.command.annotation.CommandHandler;
-import io.mifos.core.command.annotation.EventEmitter;
-import io.mifos.core.lang.ServiceException;
+import io.mifos.portfolio.service.internal.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
+import java.util.Optional;
+
/**
* @author Myrle Krantz
*/
@@ -41,13 +40,16 @@
public class ChargeDefinitionCommandHandler {
private final ProductRepository productRepository;
private final ChargeDefinitionRepository chargeDefinitionRepository;
+ private final BalanceSegmentRepository balanceSegmentRepository;
@Autowired
public ChargeDefinitionCommandHandler(
- final ProductRepository productRepository,
- final ChargeDefinitionRepository chargeDefinitionRepository) {
+ final ProductRepository productRepository,
+ final ChargeDefinitionRepository chargeDefinitionRepository,
+ final BalanceSegmentRepository balanceSegmentRepository) {
this.productRepository = productRepository;
this.chargeDefinitionRepository = chargeDefinitionRepository;
+ this.balanceSegmentRepository = balanceSegmentRepository;
}
@SuppressWarnings("unused")
@@ -62,8 +64,10 @@
= productRepository.findByIdentifier(productIdentifier)
.orElseThrow(() -> ServiceException.badRequest("The given product identifier does not refer to a product {0}", productIdentifier));
+ final SegmentRange segmentRange = getSegmentRange(chargeDefinition, productIdentifier);
+
final ChargeDefinitionEntity chargeDefinitionEntity =
- ChargeDefinitionMapper.map(productEntity, chargeDefinition);
+ ChargeDefinitionMapper.map(productEntity, chargeDefinition, segmentRange.fromSegment, segmentRange.toSegment);
chargeDefinitionRepository.save(chargeDefinitionEntity);
return new ChargeDefinitionEvent(
@@ -83,8 +87,10 @@
= chargeDefinitionRepository.findByProductIdAndChargeDefinitionIdentifier(productIdentifier, chargeDefinition.getIdentifier())
.orElseThrow(() -> ServiceException.internalError("task definition not found."));
+ final SegmentRange segmentRange = getSegmentRange(chargeDefinition, productIdentifier);
+
final ChargeDefinitionEntity chargeDefinitionEntity =
- ChargeDefinitionMapper.map(existingChargeDefinition.getProduct(), chargeDefinition);
+ ChargeDefinitionMapper.map(existingChargeDefinition.getProduct(), chargeDefinition, segmentRange.fromSegment, segmentRange.toSegment);
chargeDefinitionEntity.setId(existingChargeDefinition.getId());
chargeDefinitionEntity.setId(existingChargeDefinition.getId());
chargeDefinitionRepository.save(chargeDefinitionEntity);
@@ -112,4 +118,34 @@
command.getChargeDefinitionIdentifier());
}
+ static class SegmentRange {
+ final BalanceSegmentEntity fromSegment;
+ final BalanceSegmentEntity toSegment;
+
+ SegmentRange(final BalanceSegmentEntity fromSegment, final BalanceSegmentEntity toSegment) {
+ this.fromSegment = fromSegment;
+ this.toSegment = toSegment;
+ }
+ }
+
+ private SegmentRange getSegmentRange(final ChargeDefinition chargeDefinition, final String productIdentifier) {
+ if (chargeDefinition.getForSegmentSet() != null) {
+ final Optional<BalanceSegmentEntity> fromSegmentOptional =
+ balanceSegmentRepository.findByProductIdentifierAndSegmentSetIdentifierAndSegmentIdentifier(
+ productIdentifier,
+ chargeDefinition.getForSegmentSet(),
+ chargeDefinition.getFromSegment());
+ final Optional<BalanceSegmentEntity> toSegmentOptional =
+ balanceSegmentRepository.findByProductIdentifierAndSegmentSetIdentifierAndSegmentIdentifier(
+ productIdentifier,
+ chargeDefinition.getForSegmentSet(),
+ chargeDefinition.getFromSegment());
+
+ if (fromSegmentOptional.isPresent() && toSegmentOptional.isPresent()) {
+ return new SegmentRange(fromSegmentOptional.get(), toSegmentOptional.get());
+ }
+ }
+
+ return new SegmentRange(null, null);
+ }
}
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 6693e03..c075416 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
@@ -85,7 +85,7 @@
private void createChargeDefinition(final ProductEntity productEntity, final ChargeDefinition chargeDefinition) {
final ChargeDefinitionEntity chargeDefinitionEntity =
- ChargeDefinitionMapper.map(productEntity, chargeDefinition);
+ ChargeDefinitionMapper.map(productEntity, chargeDefinition, null, null);
chargeDefinitionRepository.save(chargeDefinitionEntity);
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/BalanceSegmentSetMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/BalanceSegmentSetMapper.java
new file mode 100644
index 0000000..63ba34f
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/BalanceSegmentSetMapper.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.portfolio.service.internal.mapper;
+
+import io.mifos.portfolio.api.v1.domain.BalanceSegmentSet;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentEntity;
+import io.mifos.portfolio.service.internal.repository.ProductEntity;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+public class BalanceSegmentSetMapper {
+ public static List<BalanceSegmentEntity> map(final BalanceSegmentSet instance, final ProductEntity product) {
+ return
+ Stream.iterate(0, i -> i+1).limit(instance.getSegmentIdentifiers().size())
+ .map(i -> {
+ final BalanceSegmentEntity ret = new BalanceSegmentEntity();
+ ret.setProduct(product);
+ ret.setSegmentSetIdentifier(instance.getIdentifier());
+ ret.setSegmentIdentifier(instance.getSegmentIdentifiers().get(i));
+ ret.setLowerBound(instance.getSegments().get(i));
+ return ret;
+ })
+ .collect(Collectors.toList());
+ }
+
+ public static Optional<BalanceSegmentSet> map(final Stream<BalanceSegmentEntity> instances) {
+ final BalanceSegmentSet ret = new BalanceSegmentSet();
+ ret.setSegments(new ArrayList<>());
+ ret.setSegmentIdentifiers(new ArrayList<>());
+ instances.sorted(Comparator.comparing(BalanceSegmentEntity::getLowerBound))
+ .forEach(seg -> {
+ ret.setIdentifier(seg.getSegmentSetIdentifier());
+ ret.getSegments().add(seg.getLowerBound());
+ ret.getSegmentIdentifiers().add(seg.getSegmentIdentifier());
+ });
+ if (ret.getSegments().isEmpty())
+ return Optional.empty();
+ else
+ return Optional.of(ret);
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
index 91958f5..f99d272 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
@@ -17,9 +17,11 @@
import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentEntity;
import io.mifos.portfolio.service.internal.repository.ChargeDefinitionEntity;
import io.mifos.portfolio.service.internal.repository.ProductEntity;
+import javax.annotation.Nullable;
import java.util.Optional;
import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
@@ -28,7 +30,11 @@
* @author Myrle Krantz
*/
public class ChargeDefinitionMapper {
- public static ChargeDefinitionEntity map(final ProductEntity productEntity, final ChargeDefinition chargeDefinition) {
+ public static ChargeDefinitionEntity map(
+ final ProductEntity productEntity,
+ final ChargeDefinition chargeDefinition,
+ @Nullable final BalanceSegmentEntity fromSegment,
+ @Nullable final BalanceSegmentEntity toSegment) {
final ChargeDefinitionEntity ret = new ChargeDefinitionEntity();
@@ -46,6 +52,11 @@
ret.setAccrualAccountDesignator(chargeDefinition.getAccrualAccountDesignator());
ret.setToAccountDesignator(chargeDefinition.getToAccountDesignator());
ret.setReadOnly(chargeDefinition.isReadOnly());
+ if (fromSegment != null && toSegment != null) {
+ ret.setSegmentSet(fromSegment.getSegmentSetIdentifier());
+ ret.setFromSegment(fromSegment.getSegmentIdentifier());
+ ret.setToSegment(toSegment.getSegmentIdentifier());
+ }
return ret;
}
@@ -66,6 +77,11 @@
ret.setAccrualAccountDesignator(from.getAccrualAccountDesignator());
ret.setToAccountDesignator(from.getToAccountDesignator());
ret.setReadOnly(Optional.ofNullable(from.getReadOnly()).orElseGet(() -> readOnlyLegacyMapper(from.getIdentifier())));
+ if (from.getSegmentSet() != null && from.getFromSegment() != null && from.getToSegment() != null) {
+ ret.setForSegmentSet(from.getSegmentSet());
+ ret.setFromSegment(from.getFromSegment());
+ ret.setToSegment(from.getToSegment());
+ }
return ret;
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/BalanceSegmentEntity.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/BalanceSegmentEntity.java
new file mode 100644
index 0000000..a395c02
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/BalanceSegmentEntity.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.portfolio.service.internal.repository;
+
+import javax.persistence.*;
+import java.math.BigDecimal;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@Entity
+@Table(name = "bastet_p_balance_segs")
+public class BalanceSegmentEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "product_id", nullable = false)
+ private ProductEntity product;
+
+ @Column(name = "seg_set_identifier", nullable = false)
+ private String segmentSetIdentifier;
+
+ @Column(name = "segment_identifier", nullable = false)
+ private String segmentIdentifier;
+
+ @Column(name = "lower_bound")
+ private BigDecimal lowerBound;
+
+ public BalanceSegmentEntity() {
+ }
+
+ public BalanceSegmentEntity(ProductEntity product, String segmentSetIdentifier, String segmentIdentifier, BigDecimal lowerBound) {
+ this.product = product;
+ this.segmentSetIdentifier = segmentSetIdentifier;
+ this.segmentIdentifier = segmentIdentifier;
+ this.lowerBound = lowerBound;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public ProductEntity getProduct() {
+ return product;
+ }
+
+ public void setProduct(ProductEntity product) {
+ this.product = product;
+ }
+
+ public String getSegmentSetIdentifier() {
+ return segmentSetIdentifier;
+ }
+
+ public void setSegmentSetIdentifier(String segmentSetIdentifier) {
+ this.segmentSetIdentifier = segmentSetIdentifier;
+ }
+
+ public String getSegmentIdentifier() {
+ return segmentIdentifier;
+ }
+
+ public void setSegmentIdentifier(String segmentIdentifier) {
+ this.segmentIdentifier = segmentIdentifier;
+ }
+
+ public BigDecimal getLowerBound() {
+ return lowerBound;
+ }
+
+ public void setLowerBound(BigDecimal lowerBound) {
+ this.lowerBound = lowerBound;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BalanceSegmentEntity that = (BalanceSegmentEntity) o;
+ return Objects.equals(product, that.product) &&
+ Objects.equals(segmentSetIdentifier, that.segmentSetIdentifier) &&
+ Objects.equals(segmentIdentifier, that.segmentIdentifier);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(product, segmentSetIdentifier, segmentIdentifier);
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/BalanceSegmentRepository.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/BalanceSegmentRepository.java
new file mode 100644
index 0000000..0a4f903
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/BalanceSegmentRepository.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.portfolio.service.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 BalanceSegmentRepository extends JpaRepository<BalanceSegmentEntity, Long> {
+ Stream<BalanceSegmentEntity> findByProductIdentifierAndSegmentSetIdentifier(String productIdentifier, String segmentSetIdentifier);
+
+ Optional<BalanceSegmentEntity> findByProductIdentifierAndSegmentSetIdentifierAndSegmentIdentifier(String productIdentifier, String segmentSetIdentifier, String segmentIdentifier);
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java
index 423c871..11e154e 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java
@@ -79,6 +79,15 @@
@Column(name = "read_only")
private Boolean readOnly;
+ @Column(name = "segment_set")
+ private String segmentSet;
+
+ @Column(name = "from_segment")
+ private String fromSegment;
+
+ @Column(name = "to_segment")
+ private String toSegment;
+
public ChargeDefinitionEntity() {
}
@@ -202,6 +211,30 @@
this.readOnly = readOnly;
}
+ public String getSegmentSet() {
+ return segmentSet;
+ }
+
+ public void setSegmentSet(String segmentSet) {
+ this.segmentSet = segmentSet;
+ }
+
+ public String getFromSegment() {
+ return fromSegment;
+ }
+
+ public void setFromSegment(String fromSegment) {
+ this.fromSegment = fromSegment;
+ }
+
+ public String getToSegment() {
+ return toSegment;
+ }
+
+ public void setToSegment(String toSegment) {
+ this.toSegment = toSegment;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/service/BalanceSegmentSetService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/BalanceSegmentSetService.java
new file mode 100644
index 0000000..ee558a6
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/service/BalanceSegmentSetService.java
@@ -0,0 +1,53 @@
+/*
+ * 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.service;
+
+import io.mifos.portfolio.api.v1.domain.BalanceSegmentSet;
+import io.mifos.portfolio.service.internal.mapper.BalanceSegmentSetMapper;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class BalanceSegmentSetService {
+ private final BalanceSegmentRepository balanceSegmentRepository;
+
+ @Autowired
+ public BalanceSegmentSetService(
+ final BalanceSegmentRepository balanceSegmentRepository) {
+ this.balanceSegmentRepository = balanceSegmentRepository;
+ }
+
+ public boolean existsByIdentifier(final String productIdentifier, final String balanceSegmentSetIdentifier)
+ {
+ //TODO: replace with existsBy once we've upgraded to spring data 1.11 or later.
+ return balanceSegmentRepository
+ .findByProductIdentifierAndSegmentSetIdentifier(productIdentifier, balanceSegmentSetIdentifier)
+ .findAny().isPresent();
+ }
+
+ public Optional<BalanceSegmentSet> findByIdentifier(
+ final String productIdentifier,
+ final String balanceSegmentSetIdentifier) {
+ return BalanceSegmentSetMapper.map(balanceSegmentRepository
+ .findByProductIdentifierAndSegmentSetIdentifier(productIdentifier, balanceSegmentSetIdentifier));
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/BalanceSegmentSetRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/BalanceSegmentSetRestController.java
new file mode 100644
index 0000000..a5ca9cc
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/BalanceSegmentSetRestController.java
@@ -0,0 +1,147 @@
+/*
+ * 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.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.portfolio.api.v1.PermittableGroupIds;
+import io.mifos.portfolio.api.v1.domain.BalanceSegmentSet;
+import io.mifos.portfolio.service.internal.command.ChangeBalanceSegmentSetCommand;
+import io.mifos.portfolio.service.internal.command.CreateBalanceSegmentSetCommand;
+import io.mifos.portfolio.service.internal.command.DeleteBalanceSegmentSetCommand;
+import io.mifos.portfolio.service.internal.service.BalanceSegmentSetService;
+import io.mifos.portfolio.service.internal.service.CaseService;
+import io.mifos.portfolio.service.internal.service.ProductService;
+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("/products/{productidentifier}/balancesegmentsets/")
+public class BalanceSegmentSetRestController {
+
+ private final CommandGateway commandGateway;
+ private final ProductService productService;
+ private final BalanceSegmentSetService balanceSegmentSetService;
+ private final CaseService caseService;
+
+ public BalanceSegmentSetRestController(final CommandGateway commandGateway,
+ final ProductService productService,
+ final BalanceSegmentSetService balanceSegmentSetService,
+ final CaseService caseService) {
+ this.commandGateway = commandGateway;
+ this.productService = productService;
+ this.balanceSegmentSetService = balanceSegmentSetService;
+ this.caseService = caseService;
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
+ @RequestMapping(
+ method = RequestMethod.POST,
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public @ResponseBody
+ ResponseEntity<Void> createBalanceSegmentSet(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @RequestBody @Valid final BalanceSegmentSet instance) {
+ checkThatProductExists(productIdentifier);
+ checkThatSegmentSetDoesntExist(productIdentifier, instance.getIdentifier());
+ checkProductChangeable(productIdentifier);
+
+ this.commandGateway.process(new CreateBalanceSegmentSetCommand(productIdentifier, instance));
+ return new ResponseEntity<>(HttpStatus.ACCEPTED);
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
+ @RequestMapping(
+ value = "{balancesegmentsetidentifier}",
+ method = RequestMethod.GET,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public @ResponseBody BalanceSegmentSet getBalanceSegmentSet(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("balancesegmentsetidentifier") final String balanceSegmentSetIdentifier) {
+ return balanceSegmentSetService.findByIdentifier(productIdentifier, balanceSegmentSetIdentifier)
+ .orElseThrow(() -> ServiceException.notFound(
+ "Segment set with identifier ''{0}.{1}'' doesn''t exist.", productIdentifier, balanceSegmentSetIdentifier));
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
+ @RequestMapping(
+ value = "{balancesegmentsetidentifier}",
+ method = RequestMethod.PUT,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public @ResponseBody ResponseEntity<Void> changeBalanceSegmentSet(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("balancesegmentsetidentifier") final String balanceSegmentSetIdentifier,
+ @RequestBody @Valid BalanceSegmentSet balanceSegmentSet) {
+ checkThatProductAndBalanceSegmentSetExist(productIdentifier, balanceSegmentSetIdentifier);
+ checkProductChangeable(productIdentifier);
+
+ if (!balanceSegmentSetIdentifier.equals(balanceSegmentSet.getIdentifier()))
+ throw ServiceException.badRequest("Instance identifier may not be changed.");
+
+ this.commandGateway.process(new ChangeBalanceSegmentSetCommand(productIdentifier, balanceSegmentSet));
+ return new ResponseEntity<>(HttpStatus.ACCEPTED);
+ }
+
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
+ @RequestMapping(
+ value = "{balancesegmentsetidentifier}",
+ method = RequestMethod.DELETE,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public @ResponseBody ResponseEntity<Void> deleteBalanceSegmentSet(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("balancesegmentsetidentifier") final String balanceSegmentSetIdentifier) {
+ checkThatProductAndBalanceSegmentSetExist(productIdentifier, balanceSegmentSetIdentifier);
+ checkProductChangeable(productIdentifier);
+
+ this.commandGateway.process(new DeleteBalanceSegmentSetCommand(productIdentifier, balanceSegmentSetIdentifier));
+ return new ResponseEntity<>(HttpStatus.ACCEPTED);
+ }
+
+ private void checkThatProductExists(final String productIdentifier) {
+ if (!productService.existsByIdentifier(productIdentifier))
+ throw ServiceException.notFound("Product with identifier ''{0}'' doesn''t exist.", productIdentifier);
+ }
+
+ private void checkThatSegmentSetDoesntExist(final String productIdentifier, final String segmentSetIdentifier) {
+ if (balanceSegmentSetService.existsByIdentifier(productIdentifier, segmentSetIdentifier))
+ throw ServiceException.notFound("Segment set with identifier ''{0}.{1}'' already exists.", productIdentifier, segmentSetIdentifier);
+ }
+
+ private void checkThatProductAndBalanceSegmentSetExist(final String productIdentifier, final String segmentSetIdentifier) {
+ if (!balanceSegmentSetService.existsByIdentifier(productIdentifier, segmentSetIdentifier))
+ throw ServiceException.notFound("Segment set with identifier ''{0}.{1}'' doesn''t exist.", productIdentifier, segmentSetIdentifier);
+ }
+
+ private void checkProductChangeable(final String productIdentifier) {
+ if (caseService.existsByProductIdentifier(productIdentifier))
+ throw ServiceException.conflict("Cases exist for product with the identifier ''{0}''. Product cannot be changed.", productIdentifier);
+ }
+}
diff --git a/service/src/main/resources/db/migrations/mariadb/V7__balance_segment_sets.sql b/service/src/main/resources/db/migrations/mariadb/V7__balance_segment_sets.sql
new file mode 100644
index 0000000..a9a9399
--- /dev/null
+++ b/service/src/main/resources/db/migrations/mariadb/V7__balance_segment_sets.sql
@@ -0,0 +1,30 @@
+--
+-- 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_p_balance_segs (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ seg_set_identifier VARCHAR(32) NOT NULL,
+ segment_identifier VARCHAR(32) NOT NULL,
+ product_id BIGINT NOT NULL,
+ lower_bound DECIMAL(19,4) NOT NULL,
+ CONSTRAINT bastet_p_balance_segs_pk PRIMARY KEY (id),
+ CONSTRAINT bastet_p_balance_segs_uq UNIQUE (product_id, seg_set_identifier, segment_identifier),
+ CONSTRAINT bastet_p_balance_segs_fk FOREIGN KEY (product_id) REFERENCES bastet_products (id)
+);
+
+ALTER TABLE bastet_p_chrg_defs ADD COLUMN segment_set VARCHAR(32) NULL DEFAULT NULL;
+ALTER TABLE bastet_p_chrg_defs ADD COLUMN from_segment VARCHAR(32) NULL DEFAULT NULL;
+ALTER TABLE bastet_p_chrg_defs ADD COLUMN to_segment VARCHAR(32) NULL DEFAULT NULL;
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ChargeRangeTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/ChargeRangeTest.java
new file mode 100644
index 0000000..a581c78
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ChargeRangeTest.java
@@ -0,0 +1,25 @@
+package io.mifos.individuallending.internal.service;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.Optional;
+
+public class ChargeRangeTest {
+ @Test
+ public void amountIsWithinRange() throws Exception {
+ final ChargeRange testSubject1 = new ChargeRange(BigDecimal.TEN, Optional.empty());
+ Assert.assertFalse(testSubject1.amountIsWithinRange(BigDecimal.ZERO));
+ Assert.assertFalse(testSubject1.amountIsWithinRange(BigDecimal.ONE));
+ Assert.assertTrue(testSubject1.amountIsWithinRange(BigDecimal.TEN));
+ Assert.assertTrue(testSubject1.amountIsWithinRange(BigDecimal.TEN.add(BigDecimal.ONE)));
+
+ final ChargeRange testSubject2 = new ChargeRange(BigDecimal.ZERO, Optional.of(BigDecimal.TEN));
+ Assert.assertTrue(testSubject2.amountIsWithinRange(BigDecimal.ZERO));
+ Assert.assertTrue(testSubject2.amountIsWithinRange(BigDecimal.ONE));
+ Assert.assertFalse(testSubject2.amountIsWithinRange(BigDecimal.TEN));
+ Assert.assertFalse(testSubject2.amountIsWithinRange(BigDecimal.TEN.add(BigDecimal.ONE)));
+ }
+
+}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
new file mode 100644
index 0000000..eab6828
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
@@ -0,0 +1,91 @@
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import static io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR;
+import static io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR;
+
+@RunWith(Parameterized.class)
+public class CostComponentServiceTest {
+ private static class TestCase {
+ final String description;
+ ChargeProportionalDesignator chargeProportionalDesignator = ChargeProportionalDesignator.NOT_PROPORTIONAL;
+ BigDecimal maximumBalance = BigDecimal.ZERO;
+ BigDecimal runningBalance = BigDecimal.ZERO;
+ BigDecimal loanPaymentSize = BigDecimal.ZERO;
+ BigDecimal expectedAmount = BigDecimal.ZERO;
+
+ private TestCase(String description) {
+ this.description = description;
+ }
+
+ TestCase chargeProportionalDesignator(ChargeProportionalDesignator chargeProportionalDesignator) {
+ this.chargeProportionalDesignator = chargeProportionalDesignator;
+ return this;
+ }
+
+ TestCase maximumBalance(BigDecimal maximumBalance) {
+ this.maximumBalance = maximumBalance;
+ return this;
+ }
+
+ TestCase runningBalance(BigDecimal runningBalance) {
+ this.runningBalance = runningBalance;
+ return this;
+ }
+
+ TestCase loanPaymentSize(BigDecimal loanPaymentSize) {
+ this.loanPaymentSize = loanPaymentSize;
+ return this;
+ }
+
+ TestCase expectedAmount(BigDecimal expectedAmount) {
+ this.expectedAmount = expectedAmount;
+ return this;
+ }
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<CostComponentServiceTest.TestCase> ret = new ArrayList<>();
+ ret.add(new TestCase("simple"));
+ ret.add(new TestCase("distribution fee")
+ .chargeProportionalDesignator(PRINCIPAL_ADJUSTMENT_DESIGNATOR)
+ .maximumBalance(BigDecimal.valueOf(2000))
+ .loanPaymentSize(BigDecimal.valueOf(-2000))
+ .expectedAmount(BigDecimal.valueOf(2000)));
+ ret.add(new TestCase("origination fee")
+ .chargeProportionalDesignator(RUNNING_BALANCE_DESIGNATOR)
+ .runningBalance(BigDecimal.valueOf(5000))
+ .expectedAmount(BigDecimal.valueOf(5000)));
+ return ret;
+ }
+
+ private final CostComponentServiceTest.TestCase testCase;
+
+ public CostComponentServiceTest(final CostComponentServiceTest.TestCase testCase) {
+ this.testCase = testCase;
+ }
+
+ @Test
+ public void getAmountProportionalTo() {
+ final BigDecimal amount = CostComponentService.getAmountProportionalTo(
+ testCase.chargeProportionalDesignator,
+ testCase.maximumBalance,
+ testCase.runningBalance,
+ testCase.loanPaymentSize,
+ Collections.emptyMap());
+
+ Assert.assertEquals(testCase.expectedAmount, amount);
+ }
+
+}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java b/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
index 7f73106..92002b7 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
@@ -28,6 +28,7 @@
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import java.util.Random;
import static java.math.BigDecimal.ROUND_HALF_EVEN;
@@ -107,7 +108,7 @@
chargeDefinition.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN);
chargeDefinition.setAccrualAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
chargeDefinition.setToAccountDesignator(AccountDesignators.INTEREST_INCOME);
- return new ScheduledCharge(scheduledAction, chargeDefinition);
+ return new ScheduledCharge(scheduledAction, chargeDefinition, Optional.empty());
}
static Period getPeriod(final LocalDate initialDate, final int periodBeginDelta, final int periodLength) {
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
index 1bb13b1..fc31ae7 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
@@ -24,6 +24,7 @@
import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.portfolio.api.v1.domain.*;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
import io.mifos.portfolio.service.internal.repository.CaseEntity;
import io.mifos.portfolio.service.internal.repository.ProductEntity;
import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
@@ -31,6 +32,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import org.mockito.Matchers;
import org.mockito.Mockito;
import java.math.BigDecimal;
@@ -47,6 +49,7 @@
*/
@RunWith(Parameterized.class)
public class IndividualLoanServiceTest {
+
private static class ActionDatePair {
final Action action;
final LocalDate localDate;
@@ -171,6 +174,7 @@
private final TestCase testCase;
private final IndividualLoanService testSubject;
+ private final ScheduledChargesService scheduledChargesService;
private final Map<String, List<ChargeDefinition>> chargeDefinitionsByChargeAction;
private final Map<String, List<ChargeDefinition>> chargeDefinitionsByAccrueAction;
@@ -282,7 +286,12 @@
Mockito.doReturn(chargeDefinitionsByChargeAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByChargeAction(testCase.productIdentifier);
Mockito.doReturn(chargeDefinitionsByAccrueAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByAccrueAction(testCase.productIdentifier);
- testSubject = new IndividualLoanService(chargeDefinitionServiceMock);
+ final BalanceSegmentRepository balanceSegmentRepositoryMock = Mockito.mock(BalanceSegmentRepository.class);
+ Mockito.doReturn(Stream.empty()).when(balanceSegmentRepositoryMock).findByProductIdentifierAndSegmentSetIdentifier(Matchers.anyString(), Matchers.anyString());
+
+ scheduledChargesService = new ScheduledChargesService(chargeDefinitionServiceMock, balanceSegmentRepositoryMock);
+
+ testSubject = new IndividualLoanService(scheduledChargesService);
}
@Test
@@ -376,7 +385,7 @@
@Test
public void getScheduledCharges() {
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
- final List<ScheduledCharge> scheduledCharges = testSubject.getScheduledCharges(testCase.productIdentifier,
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(testCase.productIdentifier,
scheduledActions);
final List<LocalDate> interestCalculationDates = scheduledCharges.stream()
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargesServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargesServiceTest.java
new file mode 100644
index 0000000..032e223
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledChargesServiceTest.java
@@ -0,0 +1,111 @@
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentEntity;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
+import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.Mockito;
+
+import java.math.BigDecimal;
+import java.util.*;
+
+@RunWith(Parameterized.class)
+public class ScheduledChargesServiceTest {
+
+ private static final String PRODUCT_IDENTIFIER = "a";
+ private static final String SEGMENT_SET_IDENTIFIER = "b";
+
+ static class TestCase {
+ final String description;
+ List<BalanceSegmentEntity> balanceSegmentEntities = Collections.emptyList();
+ String fromSegment = null;
+ String toSegment = null;
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ Optional<ChargeRange> expectedResult = Optional.empty();
+
+ private TestCase(String description) {
+ this.description = description;
+ }
+
+ TestCase balanceSegmentEntities(List<BalanceSegmentEntity> balanceSegmentEntities) {
+ this.balanceSegmentEntities = balanceSegmentEntities;
+ return this;
+ }
+
+ TestCase fromSegment(String fromSegment) {
+ this.fromSegment = fromSegment;
+ return this;
+ }
+
+ TestCase toSegment(String toSegment) {
+ this.toSegment = toSegment;
+ return this;
+ }
+
+ TestCase expectedResult(@SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional<ChargeRange> expectedResult) {
+ this.expectedResult = expectedResult;
+ return this;
+ }
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<ScheduledChargesServiceTest.TestCase> ret = new ArrayList<>();
+ ret.add(new ScheduledChargesServiceTest.TestCase("simple"));
+ ret.add(new TestCase("two segments, one referenced")
+ .balanceSegmentEntities(Arrays.asList(
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "first", BigDecimal.ZERO),
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "second", BigDecimal.TEN)))
+ .fromSegment("first")
+ .toSegment("first")
+ .expectedResult(Optional.of(new ChargeRange(BigDecimal.ZERO, Optional.of(BigDecimal.TEN))))
+ );
+ ret.add(new TestCase("two segments, both referenced")
+ .balanceSegmentEntities(Arrays.asList(
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "lower", BigDecimal.ZERO),
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "higher", BigDecimal.TEN)))
+ .fromSegment("lower")
+ .toSegment("higher")
+ .expectedResult(Optional.of(new ChargeRange(BigDecimal.ZERO, Optional.empty())))
+ );
+ ret.add(new TestCase("two segments, one mis-referenced")
+ .balanceSegmentEntities(Arrays.asList(
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "first", BigDecimal.ZERO),
+ new BalanceSegmentEntity(null, SEGMENT_SET_IDENTIFIER, "second", BigDecimal.TEN)))
+ .fromSegment("first")
+ .toSegment("second2")
+ .expectedResult(Optional.empty())
+ );
+ return ret;
+ }
+
+ private final ScheduledChargesServiceTest.TestCase testCase;
+
+ public ScheduledChargesServiceTest(final ScheduledChargesServiceTest.TestCase testCase) {
+ this.testCase = testCase;
+ }
+
+ @Test
+ public void findChargeRange() throws Exception {
+
+ final ChargeDefinitionService chargeDefinitionServiceMock = Mockito.mock(ChargeDefinitionService.class);
+ final BalanceSegmentRepository balanceSegmentRepositoryMock = Mockito.mock(BalanceSegmentRepository.class);
+
+ Mockito.doReturn(testCase.balanceSegmentEntities.stream())
+ .when(balanceSegmentRepositoryMock)
+ .findByProductIdentifierAndSegmentSetIdentifier(PRODUCT_IDENTIFIER, SEGMENT_SET_IDENTIFIER);
+
+ final ScheduledChargesService testSubject = new ScheduledChargesService(chargeDefinitionServiceMock, balanceSegmentRepositoryMock);
+ final ChargeDefinition chargeDefinition = new ChargeDefinition();
+
+ chargeDefinition.setForSegmentSet(SEGMENT_SET_IDENTIFIER);
+ chargeDefinition.setFromSegment(testCase.fromSegment);
+ chargeDefinition.setToSegment(testCase.toSegment);
+ final Optional<ChargeRange> result = testSubject.findChargeRange(PRODUCT_IDENTIFIER, chargeDefinition);
+ Assert.assertEquals(testCase.expectedResult, result);
+ }
+}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/portfolio/service/internal/command/handler/ChargeDefinitionCommandHandlerTest.java b/service/src/test/java/io/mifos/portfolio/service/internal/command/handler/ChargeDefinitionCommandHandlerTest.java
index 7b419c0..ecd74d2 100644
--- a/service/src/test/java/io/mifos/portfolio/service/internal/command/handler/ChargeDefinitionCommandHandlerTest.java
+++ b/service/src/test/java/io/mifos/portfolio/service/internal/command/handler/ChargeDefinitionCommandHandlerTest.java
@@ -16,6 +16,7 @@
package io.mifos.portfolio.service.internal.command.handler;
import io.mifos.portfolio.service.internal.command.DeleteProductChargeDefinitionCommand;
+import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
import io.mifos.portfolio.service.internal.repository.ChargeDefinitionRepository;
import io.mifos.core.lang.ServiceException;
import org.junit.Assert;
@@ -37,7 +38,9 @@
.when(chargeDefinitionRepositoryMock)
.findByProductIdAndChargeDefinitionIdentifier(productIdentifier, chargeDefinitionIdentifier);
- final ChargeDefinitionCommandHandler testSubject = new ChargeDefinitionCommandHandler(null, chargeDefinitionRepositoryMock);
+ final BalanceSegmentRepository balanceSegmentRepository = Mockito.mock(BalanceSegmentRepository.class);
+
+ final ChargeDefinitionCommandHandler testSubject = new ChargeDefinitionCommandHandler(null, chargeDefinitionRepositoryMock, balanceSegmentRepository);
try {
testSubject.process(new DeleteProductChargeDefinitionCommand(productIdentifier, chargeDefinitionIdentifier));