Merge pull request #26 from myrle-krantz/develop
open command processing and scale loosening.
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParameters.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParameters.java
index 9e5ad0e..5fc2e9c 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParameters.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParameters.java
@@ -31,7 +31,7 @@
* @author Myrle Krantz
*/
@SuppressWarnings({"WeakerAccess", "unused"})
-@ScriptAssert(lang = "javascript", script = "_this.maximumBalance !== null && _this.maximumBalance.scale() == 4")
+@ScriptAssert(lang = "javascript", script = "_this.maximumBalance !== null && _this.maximumBalance.scale() <= 4")
public final class CaseParameters {
@ValidIdentifier
private String customerIdentifier;
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/workflow/Action.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/workflow/Action.java
index e91750d..cd51961 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/workflow/Action.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/workflow/Action.java
@@ -23,14 +23,24 @@
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public enum Action {
- OPEN,
- DENY,
- APPROVE,
- DISBURSE,
- APPLY_INTEREST,
- ACCEPT_PAYMENT,
- MARK_LATE,
- WRITE_OFF,
- CLOSE,
- RECOVER
+ OPEN("CHRG"),
+ DENY("CHRG"),
+ APPROVE("ACCO"),
+ DISBURSE("CDIS"),
+ APPLY_INTEREST("INTR"),
+ ACCEPT_PAYMENT("PPAY"),
+ MARK_LATE("ICCT"),
+ WRITE_OFF("ICCT"),
+ CLOSE("ICCT"),
+ RECOVER("ICCT");
+
+ private final String transactionType;
+
+ Action(final String transactionType) {
+ this.transactionType = transactionType;
+ }
+
+ public String getTransactionType() {
+ return transactionType;
+ }
}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
index 4cb0cc6..708b65a 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
@@ -15,29 +15,39 @@
*/
package io.mifos.portfolio.api.v1.domain;
-import org.hibernate.validator.constraints.NotBlank;
-
+import javax.validation.Valid;
import java.util.List;
+import java.util.Objects;
/**
* @author Myrle Krantz
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public final class Command {
+ @Valid
private List<AccountAssignment> oneTimeAccountAssignments;
- private String comment;
+
+ private String note;
private String createdOn;
private String createdBy;
public Command() {
}
- public String getComment() {
- return comment;
+ public List<AccountAssignment> getOneTimeAccountAssignments() {
+ return oneTimeAccountAssignments;
}
- public void setComment(String comment) {
- this.comment = comment;
+ public void setOneTimeAccountAssignments(List<AccountAssignment> oneTimeAccountAssignments) {
+ this.oneTimeAccountAssignments = oneTimeAccountAssignments;
+ }
+
+ public String getNote() {
+ return note;
+ }
+
+ public void setNote(String note) {
+ this.note = note;
}
public String getCreatedOn() {
@@ -55,4 +65,30 @@
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Command command = (Command) o;
+ return Objects.equals(oneTimeAccountAssignments, command.oneTimeAccountAssignments) &&
+ Objects.equals(note, command.note) &&
+ Objects.equals(createdOn, command.createdOn) &&
+ Objects.equals(createdBy, command.createdBy);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(oneTimeAccountAssignments, note, createdOn, createdBy);
+ }
+
+ @Override
+ public String toString() {
+ return "Command{" +
+ "oneTimeAccountAssignments=" + oneTimeAccountAssignments +
+ ", note='" + note + '\'' +
+ ", createdOn='" + createdOn + '\'' +
+ ", createdBy='" + createdBy + '\'' +
+ '}';
+ }
}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/InterestRange.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/InterestRange.java
index 9bb75c4..d119bd7 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/InterestRange.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/InterestRange.java
@@ -25,7 +25,7 @@
* @author Myrle Krantz
*/
@SuppressWarnings({"WeakerAccess", "unused"})
-@ScriptAssert(lang = "javascript", script = "_this.maximum != null && _this.minimum != null && _this.maximum.compareTo(_this.minimum) >= 0 && _this.minimum.scale() == 2 && _this.maximum.scale() == 2")
+@ScriptAssert(lang = "javascript", script = "_this.maximum != null && _this.minimum != null && _this.maximum.compareTo(_this.minimum) >= 0 && _this.minimum.scale() <= 2 && _this.maximum.scale() <= 2")
public class InterestRange {
@DecimalMin(value = "0.00")
@DecimalMax(value = "999.99")
diff --git a/api/src/test/java/io/mifos/Fixture.java b/api/src/test/java/io/mifos/Fixture.java
index a022fe6..c253395 100644
--- a/api/src/test/java/io/mifos/Fixture.java
+++ b/api/src/test/java/io/mifos/Fixture.java
@@ -35,6 +35,16 @@
*/
@SuppressWarnings("WeakerAccess")
public class Fixture {
+ static final String INCOME_LEDGER_IDENTIFIER = "1000";
+ static final String FEES_AND_CHARGES_LEDGER_IDENTIFIER = "1300";
+ static final String CASH_LEDGER_IDENTIFIER = "7300";
+ static final String PENDING_DISBURSAL_LEDGER_IDENTIFIER = "7320";
+ static final String CUSTOMER_LOAN_LEDGER_IDENTIFIER = "7353";
+ static final String LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER = "7310";
+ static final String LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER = "1310";
+ static final String PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER = "1312";
+ static final String TELLER_ONE_ACCOUNT_IDENTIFIER = "7352";
+
static int uniquenessSuffix = 0;
static public Product getTestProduct() {
@@ -52,15 +62,19 @@
product.setMinorCurrencyUnitDigits(2);
final Set<AccountAssignment> accountAssignments = new HashSet<>();
- accountAssignments.add(new AccountAssignment(PENDING_DISBURSAL, "001-003"));
- accountAssignments.add(new AccountAssignment(PROCESSING_FEE_INCOME, "001-004"));
- accountAssignments.add(new AccountAssignment(ORIGINATION_FEE_INCOME, "001-004"));
+ accountAssignments.add(new AccountAssignment(PENDING_DISBURSAL, PENDING_DISBURSAL_LEDGER_IDENTIFIER));
+ accountAssignments.add(new AccountAssignment(PROCESSING_FEE_INCOME, PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER));
+ accountAssignments.add(new AccountAssignment(ORIGINATION_FEE_INCOME, LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(DISBURSEMENT_FEE_INCOME, "001-004"));
accountAssignments.add(new AccountAssignment(INTEREST_INCOME, "001-005"));
accountAssignments.add(new AccountAssignment(INTEREST_ACCRUAL, "001-007"));
accountAssignments.add(new AccountAssignment(LATE_FEE_INCOME, "001-008"));
accountAssignments.add(new AccountAssignment(LATE_FEE_ACCRUAL, "001-009"));
accountAssignments.add(new AccountAssignment(ARREARS_ALLOWANCE, "001-010"));
+ //accountAssignments.add(new AccountAssignment(ENTRY, ...));
+ // Don't assign entry account in test since it usually will not be assigned IRL.
+ accountAssignments.add(new AccountAssignment(LOAN_FUNDS_SOURCE, LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER));
+ accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN, "001-013"));
product.setAccountAssignments(accountAssignments);
final ProductParameters productParameters = new ProductParameters();
diff --git a/api/src/test/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParametersTest.java b/api/src/test/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParametersTest.java
index decfa89..89973c8 100644
--- a/api/src/test/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParametersTest.java
+++ b/api/src/test/java/io/mifos/individuallending/api/v1/domain/caseinstance/CaseParametersTest.java
@@ -58,9 +58,12 @@
ret.add(new ValidationTestCase<CaseParameters>("nullBalanceRange")
.adjustment(x -> x.setMaximumBalance(null))
.valid(false));
- ret.add(new ValidationTestCase<CaseParameters>("badBalanceRangeScale")
+ ret.add(new ValidationTestCase<CaseParameters>("tooLargeBalanceRangeScale")
.adjustment(x -> x.setMaximumBalance(BigDecimal.TEN.setScale(5, BigDecimal.ROUND_FLOOR)))
.valid(false));
+ ret.add(new ValidationTestCase<CaseParameters>("smallerBalanceRangeScale")
+ .adjustment(x -> x.setMaximumBalance(BigDecimal.TEN.setScale(3, BigDecimal.ROUND_FLOOR)))
+ .valid(true));
ret.add(new ValidationTestCase<CaseParameters>("invalid payment cycle unit")
.adjustment(x -> x.getPaymentCycle().setTemporalUnit(ChronoUnit.SECONDS))
.valid(false));
diff --git a/api/src/test/java/io/mifos/portfolio/api/v1/domain/CommandTest.java b/api/src/test/java/io/mifos/portfolio/api/v1/domain/CommandTest.java
index 20cf510..5e4430b 100644
--- a/api/src/test/java/io/mifos/portfolio/api/v1/domain/CommandTest.java
+++ b/api/src/test/java/io/mifos/portfolio/api/v1/domain/CommandTest.java
@@ -22,6 +22,9 @@
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.PROCESSING_FEE_ID;
/**
* @author Myrle Krantz
@@ -34,13 +37,21 @@
@Override
protected Command createValidTestSubject() {
- return new Command();
+ final Command ret = new Command();
+ ret.setOneTimeAccountAssignments(Collections.emptyList());
+ return ret;
}
@Parameterized.Parameters
public static Collection testCases() {
final Collection<ValidationTestCase> ret = new ArrayList<>();
ret.add(new ValidationTestCase<Command>("valid"));
+ ret.add(new ValidationTestCase<Command>("invalidAccountAssignment")
+ .adjustment(x -> x.setOneTimeAccountAssignments(Collections.singletonList(new AccountAssignment("", ""))))
+ .valid(false));
+ ret.add(new ValidationTestCase<Command>("validAccountAssignment")
+ .adjustment(x -> x.setOneTimeAccountAssignments(Collections.singletonList(new AccountAssignment(PROCESSING_FEE_ID, "7534"))))
+ .valid(true));
return ret;
}
}
diff --git a/api/src/test/java/io/mifos/portfolio/api/v1/domain/InterestRangeTest.java b/api/src/test/java/io/mifos/portfolio/api/v1/domain/InterestRangeTest.java
index 666aeac..0f4a851 100644
--- a/api/src/test/java/io/mifos/portfolio/api/v1/domain/InterestRangeTest.java
+++ b/api/src/test/java/io/mifos/portfolio/api/v1/domain/InterestRangeTest.java
@@ -66,7 +66,14 @@
x.setMaximum(BigDecimal.valueOf(5L).setScale(2, BigDecimal.ROUND_UNNECESSARY));
})
.valid(false));
+ ret.add(new ValidationTestCase<InterestRange>("too large scale")
+ .adjustment(x ->
+ x.setMinimum(x.getMinimum().setScale(3, BigDecimal.ROUND_UNNECESSARY)))
+ .valid(false));
+ ret.add(new ValidationTestCase<InterestRange>("smaller scale")
+ .adjustment(x ->
+ x.setMinimum(x.getMinimum().setScale(1, BigDecimal.ROUND_HALF_EVEN)))
+ .valid(true));
return ret;
}
-
}
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
index 0758a33..4498f46 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -25,9 +25,10 @@
import io.mifos.core.test.listener.EnableEventRecording;
import io.mifos.core.test.listener.EventRecorder;
import io.mifos.individuallending.api.v1.client.IndividualLending;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
import io.mifos.portfolio.api.v1.client.PortfolioManager;
-import io.mifos.portfolio.api.v1.domain.Case;
-import io.mifos.portfolio.api.v1.domain.Product;
+import io.mifos.portfolio.api.v1.domain.*;
import io.mifos.portfolio.api.v1.events.CaseEvent;
import io.mifos.portfolio.api.v1.events.EventConstants;
import io.mifos.portfolio.service.config.PortfolioServiceConfiguration;
@@ -55,8 +56,12 @@
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doReturn;
@@ -128,10 +133,13 @@
@MockBean
RhythmAdapter rhythmAdapter;
+ @MockBean
+ LedgerManager ledgerManager;
+
@Before
public void prepTest() {
userContext = this.tenantApplicationSecurityEnvironment.createAutoUserContext(TEST_USER);
- setupMockAccountingAdapter();
+ AccountingFixture.mockAccountingPrereqs(ledgerManager);
}
@After
@@ -148,9 +156,6 @@
}
}
- private void setupMockAccountingAdapter() {
- }
-
Product createProduct() throws InterruptedException {
return createAdjustedProduct(x -> {});
}
@@ -188,4 +193,46 @@
return caseInstance;
}
+
+ void checkStateTransfer(final String productIdentifier,
+ final String caseIdentifier,
+ final Action action,
+ final List<AccountAssignment> oneTimeAccountAssignments,
+ final String event,
+ final Case.State nextState) throws InterruptedException {
+ final Command command = new Command();
+ command.setOneTimeAccountAssignments(oneTimeAccountAssignments);
+ portfolioManager.executeCaseCommand(productIdentifier, caseIdentifier, action.name(), command);
+
+ Assert.assertTrue(eventRecorder.waitForMatch(event,
+ (IndividualLoanCommandEvent x) -> individualLoanCommandEventMatches(x, productIdentifier, caseIdentifier)));
+
+ final Case customerCase = portfolioManager.getCase(productIdentifier, caseIdentifier);
+ Assert.assertEquals(customerCase.getCurrentState(), nextState.name());
+ }
+
+ boolean individualLoanCommandEventMatches(
+ final IndividualLoanCommandEvent event,
+ final String productIdentifier,
+ final String caseIdentifier)
+ {
+ return event.getProductIdentifier().equals(productIdentifier) &&
+ event.getCaseIdentifier().equals(caseIdentifier);
+ }
+
+ void checkNextActionsCorrect(final String productIdentifier, final String customerCaseIdentifier, final Action... nextActions)
+ {
+ final Set<String> actionList = Arrays.stream(nextActions).map(Enum::name).collect(Collectors.toSet());
+ Assert.assertEquals(actionList, portfolioManager.getActionsForCase(productIdentifier, customerCaseIdentifier));
+ }
+
+ void checkCostComponentForActionCorrect(final String productIdentifier,
+ final String customerCaseIdentifier,
+ final Action action,
+ final CostComponent... expectedCostComponents) {
+ final List<CostComponent> costComponents = portfolioManager.getCostComponentsForAction(productIdentifier, customerCaseIdentifier, action.name());
+ final Set<CostComponent> setOfCostComponents = new HashSet<>(costComponents);
+ final Set<CostComponent> setOfExpectedCostComponents = new HashSet<>(Arrays.asList(expectedCostComponents));
+ Assert.assertEquals(setOfExpectedCostComponents, setOfCostComponents);
+ }
}
diff --git a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
new file mode 100644
index 0000000..3c401f3
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.portfolio;
+
+import io.mifos.accounting.api.v1.client.LedgerManager;
+import io.mifos.accounting.api.v1.domain.*;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mockito;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.math.BigDecimal;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static io.mifos.portfolio.Fixture.*;
+import static org.mockito.Matchers.argThat;
+
+/**
+ * @author Myrle Krantz
+ */
+class AccountingFixture {
+
+
+ private static Ledger cashLedger() {
+ final Ledger ret = new Ledger();
+ ret.setIdentifier(CASH_LEDGER_IDENTIFIER);
+ ret.setType(AccountType.ASSET.name());
+ return ret;
+ }
+
+ private static Ledger incomeLedger() {
+ final Ledger ret = new Ledger();
+ ret.setIdentifier(INCOME_LEDGER_IDENTIFIER);
+ ret.setType(AccountType.REVENUE.name());
+ return ret;
+ }
+
+ private static Ledger feesAndChargesLedger() {
+ final Ledger ret = new Ledger();
+ ret.setIdentifier(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
+ ret.setParentLedgerIdentifier(INCOME_LEDGER_IDENTIFIER);
+ ret.setType(AccountType.REVENUE.name());
+ return ret;
+ }
+
+ private static Ledger pendingDisbursalLedger() {
+ final Ledger ret = new Ledger();
+ ret.setIdentifier(PENDING_DISBURSAL_LEDGER_IDENTIFIER);
+ ret.setParentLedgerIdentifier(CASH_LEDGER_IDENTIFIER);
+ ret.setType(AccountType.ASSET.name());
+ return ret;
+ }
+
+ private static Ledger customerLoanLedger() {
+ final Ledger ret = new Ledger();
+ ret.setIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
+ ret.setParentLedgerIdentifier(CASH_LEDGER_IDENTIFIER);
+ ret.setType(AccountType.ASSET.name());
+ return ret;
+ }
+
+ private static Account loanFundsSourceAccount() {
+ final Account ret = new Account();
+ ret.setIdentifier(LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER);
+ ret.setLedger(CASH_LEDGER_IDENTIFIER);
+ ret.setType(AccountType.ASSET.name());
+ return ret;
+ }
+
+ private static Account processingFeeIncomeAccount() {
+ final Account ret = new Account();
+ ret.setIdentifier(PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER);
+ ret.setLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
+ ret.setType(AccountType.REVENUE.name());
+ return ret;
+ }
+
+ private static Account loanOriginationFeesIncomeAccount() {
+ final Account ret = new Account();
+ ret.setIdentifier(LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER);
+ ret.setLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
+ ret.setType(AccountType.REVENUE.name());
+ return ret;
+ }
+
+ private static Account tellerOneAccount() {
+ final Account ret = new Account();
+ ret.setIdentifier(TELLER_ONE_ACCOUNT_IDENTIFIER);
+ ret.setLedger(CASH_LEDGER_IDENTIFIER);
+ ret.setType(AccountType.ASSET.name());
+ return ret;
+ }
+
+ private static class AccountMatcher extends ArgumentMatcher<Account> {
+ private final String ledgerIdentifer;
+ private final AccountType type;
+ private Account checkedArgument;
+
+ private AccountMatcher(final String ledgerIdentifier, final AccountType type) {
+ this.ledgerIdentifer = ledgerIdentifier;
+ this.type = type;
+ this.checkedArgument = null; //Set when matches called.
+ }
+
+ @Override
+ public boolean matches(final Object argument) {
+ if (argument == null)
+ return false;
+ if (! (argument instanceof Account))
+ return false;
+
+ checkedArgument = (Account) argument;
+
+ final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+ final Set errors = validator.validate(checkedArgument, Account.class);
+
+ return errors.size() == 0 &&
+ checkedArgument.getLedger().equals(ledgerIdentifer) &&
+ checkedArgument.getType().equals(type.name()) &&
+ checkedArgument.getBalance() == 0.0;
+ }
+
+ Account getCheckedArgument() {
+ return checkedArgument;
+ }
+ }
+
+ private static class JournalEntryMatcher extends ArgumentMatcher<JournalEntry> {
+ private final String expectedFromAccountIdentifier;
+ private final String expectedToAccountIdentifier;
+ private final BigDecimal expectedAmount;
+ private JournalEntry checkedArgument;
+
+ private JournalEntryMatcher(final String expectedFromAccountIdentifier,
+ final String expectedToAccountIdentifier,
+ final BigDecimal amount) {
+ this.expectedFromAccountIdentifier = expectedFromAccountIdentifier;
+ this.expectedToAccountIdentifier = expectedToAccountIdentifier;
+ this.expectedAmount = amount;
+ this.checkedArgument = null; //Set when matches called.
+ }
+
+ @Override
+ public boolean matches(final Object argument) {
+ if (argument == null)
+ return false;
+ if (! (argument instanceof JournalEntry))
+ return false;
+
+ checkedArgument = (JournalEntry) argument;
+ final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+ final Set errors = validator.validate(checkedArgument);
+
+ final Double debitAmount = checkedArgument.getDebtors().stream()
+ .collect(Collectors.summingDouble(x -> Double.valueOf(x.getAmount())));
+
+ final Optional<String> fromAccountIdentifier = checkedArgument.getDebtors().stream().findFirst().map(Debtor::getAccountNumber);
+
+ final Double creditAmount = checkedArgument.getCreditors().stream()
+ .collect(Collectors.summingDouble(x -> Double.valueOf(x.getAmount())));
+
+ final Optional<String> toAccountIdentifier = checkedArgument.getCreditors().stream().findFirst().map(Creditor::getAccountNumber);
+
+ return (errors.size() == 0 &&
+ fromAccountIdentifier.isPresent() && fromAccountIdentifier.get().equals(expectedFromAccountIdentifier) &&
+ toAccountIdentifier.isPresent() && toAccountIdentifier.get().equals(expectedToAccountIdentifier) &&
+ creditAmount.equals(debitAmount) &&
+ creditAmount.equals(expectedAmount.doubleValue()));
+ }
+
+ JournalEntry getCheckedArgument() {
+ return checkedArgument;
+ }
+
+ @Override
+ public String toString() {
+ return "JournalEntryMatcher{" +
+ "expectedFromAccountIdentifier='" + expectedFromAccountIdentifier + '\'' +
+ ", expectedToAccountIdentifier='" + expectedToAccountIdentifier + '\'' +
+ ", expectedAmount=" + expectedAmount +
+ ", checkedArgument=" + checkedArgument +
+ '}';
+ }
+ }
+
+ static void mockAccountingPrereqs(final LedgerManager ledgerManagerMock) {
+ Mockito.doReturn(incomeLedger()).when(ledgerManagerMock).findLedger(INCOME_LEDGER_IDENTIFIER);
+ Mockito.doReturn(feesAndChargesLedger()).when(ledgerManagerMock).findLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
+ Mockito.doReturn(cashLedger()).when(ledgerManagerMock).findLedger(CASH_LEDGER_IDENTIFIER);
+ Mockito.doReturn(pendingDisbursalLedger()).when(ledgerManagerMock).findLedger(PENDING_DISBURSAL_LEDGER_IDENTIFIER);
+ Mockito.doReturn(customerLoanLedger()).when(ledgerManagerMock).findLedger(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
+ Mockito.doReturn(loanFundsSourceAccount()).when(ledgerManagerMock).findAccount(LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER);
+ Mockito.doReturn(loanOriginationFeesIncomeAccount()).when(ledgerManagerMock).findAccount(LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER);
+ Mockito.doReturn(processingFeeIncomeAccount()).when(ledgerManagerMock).findAccount(PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER);
+ Mockito.doReturn(tellerOneAccount()).when(ledgerManagerMock).findAccount(TELLER_ONE_ACCOUNT_IDENTIFIER);
+ }
+
+ static String verifyAccountCreation(final LedgerManager ledgerManager,
+ final String ledgerIdentifier,
+ final AccountType type) {
+ final AccountMatcher specifiesCorrectAccount = new AccountMatcher(ledgerIdentifier, type);
+ Mockito.verify(ledgerManager).createAccount(argThat(specifiesCorrectAccount));
+ return specifiesCorrectAccount.getCheckedArgument().getIdentifier();
+ }
+
+ static void verifyTransfer(final LedgerManager ledgerManager,
+ final String fromAccountIdentifier,
+ final String toAccountIdentifier,
+ final BigDecimal amount) {
+ final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(fromAccountIdentifier, toAccountIdentifier, amount);
+ Mockito.verify(ledgerManager).createJournalEntry(argThat(specifiesCorrectJournalEntry));
+ }
+}
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 40b60c2..b9f0893 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -36,6 +36,16 @@
@SuppressWarnings({"WeakerAccess", "unused"})
public class Fixture {
+ static final String INCOME_LEDGER_IDENTIFIER = "1000";
+ static final String FEES_AND_CHARGES_LEDGER_IDENTIFIER = "1300";
+ static final String CASH_LEDGER_IDENTIFIER = "7300";
+ static final String PENDING_DISBURSAL_LEDGER_IDENTIFIER = "7320";
+ static final String CUSTOMER_LOAN_LEDGER_IDENTIFIER = "7353";
+ static final String LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER = "7310";
+ static final String LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER = "1310";
+ static final String PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER = "1312";
+ static final String TELLER_ONE_ACCOUNT_IDENTIFIER = "7352";
+
private static int uniquenessSuffix = 0;
static public Product getTestProduct() {
@@ -53,18 +63,22 @@
product.setMinorCurrencyUnitDigits(2);
final Set<AccountAssignment> accountAssignments = new HashSet<>();
- accountAssignments.add(new AccountAssignment(PENDING_DISBURSAL, "001-003"));
- accountAssignments.add(new AccountAssignment(PROCESSING_FEE_INCOME, "001-004"));
- accountAssignments.add(new AccountAssignment(ORIGINATION_FEE_INCOME, "001-004"));
+ accountAssignments.add(new AccountAssignment(PENDING_DISBURSAL, PENDING_DISBURSAL_LEDGER_IDENTIFIER));
+ accountAssignments.add(new AccountAssignment(PROCESSING_FEE_INCOME, PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER));
+ accountAssignments.add(new AccountAssignment(ORIGINATION_FEE_INCOME, LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(DISBURSEMENT_FEE_INCOME, "001-004"));
accountAssignments.add(new AccountAssignment(INTEREST_INCOME, "001-005"));
accountAssignments.add(new AccountAssignment(INTEREST_ACCRUAL, "001-007"));
accountAssignments.add(new AccountAssignment(LATE_FEE_INCOME, "001-008"));
accountAssignments.add(new AccountAssignment(LATE_FEE_ACCRUAL, "001-009"));
accountAssignments.add(new AccountAssignment(ARREARS_ALLOWANCE, "001-010"));
- accountAssignments.add(new AccountAssignment(ENTRY, "001-011"));
- accountAssignments.add(new AccountAssignment(LOAN_FUNDS_SOURCE, "001-012"));
- accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN, "001-013"));
+ //accountAssignments.add(new AccountAssignment(ENTRY, ...));
+ // Don't assign entry account in test since it usually will not be assigned IRL.
+ accountAssignments.add(new AccountAssignment(LOAN_FUNDS_SOURCE, LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER));
+ final AccountAssignment customerLoanAccountAssignment = new AccountAssignment();
+ customerLoanAccountAssignment.setDesignator(CUSTOMER_LOAN);
+ customerLoanAccountAssignment.setLedgerIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
+ accountAssignments.add(customerLoanAccountAssignment);
product.setAccountAssignments(accountAssignments);
final ProductParameters productParameters = new ProductParameters();
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteraction.java b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteraction.java
new file mode 100644
index 0000000..c471655
--- /dev/null
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteraction.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.portfolio;
+
+import com.google.gson.Gson;
+import io.mifos.accounting.api.v1.domain.AccountType;
+import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.portfolio.api.v1.domain.*;
+import io.mifos.portfolio.api.v1.events.ChargeDefinitionEvent;
+import io.mifos.portfolio.api.v1.events.EventConstants;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
+import static io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants.APPROVE_INDIVIDUALLOAN_CASE;
+import static io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE;
+import static io.mifos.portfolio.Fixture.*;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestAccountingInteraction extends AbstractPortfolioTest {
+
+ @Test
+ public void testLoanApproval() throws InterruptedException {
+ //Create product and set charges to fixed fees.
+ final Product product = createProduct();
+
+ final ChargeDefinition processingFee = portfolioManager.getChargeDefinition(product.getIdentifier(), PROCESSING_FEE_ID);
+ processingFee.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+ processingFee.setAmount(BigDecimal.valueOf(10_0000, 4));
+ portfolioManager.changeChargeDefinition(product.getIdentifier(), PROCESSING_FEE_ID, processingFee);
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CHARGE_DEFINITION,
+ new ChargeDefinitionEvent(product.getIdentifier(), PROCESSING_FEE_ID)));
+
+ final ChargeDefinition loanOriginationFee = portfolioManager.getChargeDefinition(product.getIdentifier(), LOAN_ORIGINATION_FEE_ID);
+ loanOriginationFee.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+ loanOriginationFee.setAmount(BigDecimal.valueOf(100_0000, 4));
+ portfolioManager.changeChargeDefinition(product.getIdentifier(), LOAN_ORIGINATION_FEE_ID, loanOriginationFee);
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CHARGE_DEFINITION,
+ new ChargeDefinitionEvent(product.getIdentifier(), LOAN_ORIGINATION_FEE_ID)));
+
+ portfolioManager.enableProduct(product.getIdentifier(), true);
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_PRODUCT_ENABLE, product.getIdentifier()));
+
+
+
+ //Create case.
+ final CaseParameters caseParameters = Fixture.createAdjustedCaseParameters(x -> {});
+ final String caseParametersAsString = new Gson().toJson(caseParameters);
+ final Case customerCase = createAdjustedCase(product.getIdentifier(), x -> x.setParameters(caseParametersAsString));
+
+ //Open the case and accept a processing fee.
+ checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN);
+ checkCostComponentForActionCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN,
+ new CostComponent(processingFee.getIdentifier(), processingFee.getAmount()));
+
+ final AccountAssignment openCommandProcessingFeeAccountAssignment = new AccountAssignment();
+ openCommandProcessingFeeAccountAssignment.setDesignator(processingFee.getFromAccountDesignator());
+ openCommandProcessingFeeAccountAssignment.setAccountIdentifier(TELLER_ONE_ACCOUNT_IDENTIFIER);
+
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN,
+ Collections.singletonList(openCommandProcessingFeeAccountAssignment),
+ OPEN_INDIVIDUALLOAN_CASE, Case.State.PENDING);
+ checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Action.DENY);
+ checkCostComponentForActionCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE,
+ new CostComponent(loanOriginationFee.getIdentifier(), loanOriginationFee.getAmount()));
+
+ AccountingFixture.verifyTransfer(ledgerManager,
+ TELLER_ONE_ACCOUNT_IDENTIFIER, PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER,
+ processingFee.getAmount()
+ );
+ }
+}
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
index e2e9122..08186c7 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
@@ -23,9 +23,7 @@
import org.junit.Assert;
import org.junit.Test;
-import java.util.Arrays;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.Collections;
import static io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants.*;
@@ -41,29 +39,29 @@
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN);
- checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN, OPEN_INDIVIDUALLOAN_CASE, Case.State.PENDING);
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN, Collections.emptyList(), OPEN_INDIVIDUALLOAN_CASE, Case.State.PENDING);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Action.DENY);
- checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, APPROVE_INDIVIDUALLOAN_CASE, Case.State.APPROVED);
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Collections.emptyList(), APPROVE_INDIVIDUALLOAN_CASE, Case.State.APPROVED);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Action.CLOSE);
- checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, DISBURSE_INDIVIDUALLOAN_CASE, Case.State.ACTIVE);
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Collections.emptyList(), DISBURSE_INDIVIDUALLOAN_CASE, Case.State.ACTIVE);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
- checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.ACCEPT_PAYMENT, ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE, Case.State.ACTIVE);
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.ACCEPT_PAYMENT, Collections.emptyList(), ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE, Case.State.ACTIVE);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
- checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.ACCEPT_PAYMENT, ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE, Case.State.ACTIVE);
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.ACCEPT_PAYMENT, Collections.emptyList(), ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE, Case.State.ACTIVE);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
- checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.CLOSE, CLOSE_INDIVIDUALLOAN_CASE, Case.State.CLOSED);
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.CLOSE, Collections.emptyList(), CLOSE_INDIVIDUALLOAN_CASE, Case.State.CLOSED);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
}
@@ -75,19 +73,19 @@
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN);
- checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN, OPEN_INDIVIDUALLOAN_CASE, Case.State.PENDING);
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN, Collections.emptyList(), OPEN_INDIVIDUALLOAN_CASE, Case.State.PENDING);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Action.DENY);
- checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, APPROVE_INDIVIDUALLOAN_CASE, Case.State.APPROVED);
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Collections.emptyList(), APPROVE_INDIVIDUALLOAN_CASE, Case.State.APPROVED);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Action.CLOSE);
- checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, DISBURSE_INDIVIDUALLOAN_CASE, Case.State.ACTIVE);
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Collections.emptyList(), DISBURSE_INDIVIDUALLOAN_CASE, Case.State.ACTIVE);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
- checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.WRITE_OFF, WRITE_OFF_INDIVIDUALLOAN_CASE, Case.State.CLOSED);
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.WRITE_OFF, Collections.emptyList(), WRITE_OFF_INDIVIDUALLOAN_CASE, Case.State.CLOSED);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
}
@@ -96,26 +94,11 @@
final Product product = createAndEnableProduct();
final Case customerCase = createCase(product.getIdentifier());
- checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN, OPEN_INDIVIDUALLOAN_CASE, Case.State.PENDING);
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN, Collections.emptyList(), OPEN_INDIVIDUALLOAN_CASE, Case.State.PENDING);
checkStateTransferFails(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, DISBURSE_INDIVIDUALLOAN_CASE, Case.State.PENDING);
}
- public void checkStateTransfer(final String productIdentifier,
- final String caseIdentifier,
- final Action action,
- final String event,
- final Case.State nextState) throws InterruptedException {
- final Command command = new Command();
- portfolioManager.executeCaseCommand(productIdentifier, caseIdentifier, action.name(), command);
-
- Assert.assertTrue(eventRecorder.waitForMatch(event,
- (IndividualLoanCommandEvent x) -> individualLoanCommandEventMatches(x, productIdentifier, caseIdentifier)));
-
- final Case customerCase = portfolioManager.getCase(productIdentifier, caseIdentifier);
- Assert.assertEquals(customerCase.getCurrentState(), nextState.name());
- }
-
public void checkStateTransferFails(final String productIdentifier,
final String caseIdentifier,
final Action action,
@@ -134,19 +117,4 @@
final Case customerCase = portfolioManager.getCase(productIdentifier, caseIdentifier);
Assert.assertEquals(customerCase.getCurrentState(), initialState.name());
}
-
- private void checkNextActionsCorrect(final String productIdentifier, final String customerCaseIdentifier, final Action... nextActions)
- {
- final Set<String> actionList = Arrays.stream(nextActions).map(Enum::name).collect(Collectors.toSet());
- Assert.assertEquals(actionList, portfolioManager.getActionsForCase(productIdentifier, customerCaseIdentifier));
- }
-
- private boolean individualLoanCommandEventMatches(
- final IndividualLoanCommandEvent event,
- final String productIdentifier,
- final String caseIdentifier)
- {
- return event.getProductIdentifier().equals(productIdentifier) &&
- event.getCaseIdentifier().equals(caseIdentifier);
- }
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingCommandDispatcher.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingCommandDispatcher.java
index 5b685b3..0faf9a9 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingCommandDispatcher.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingCommandDispatcher.java
@@ -42,31 +42,31 @@
final Action action = Action.valueOf(actionIdentifier);
switch (action) {
case OPEN:
- this.commandGateway.process(new OpenCommand(productIdentifier, caseIdentifier));
+ this.commandGateway.process(new OpenCommand(productIdentifier, caseIdentifier, command));
break;
case DENY:
- this.commandGateway.process(new DenyCommand(productIdentifier, caseIdentifier));
+ this.commandGateway.process(new DenyCommand(productIdentifier, caseIdentifier, command));
break;
case APPROVE:
- this.commandGateway.process(new ApproveCommand(productIdentifier, caseIdentifier));
+ this.commandGateway.process(new ApproveCommand(productIdentifier, caseIdentifier, command));
break;
case DISBURSE:
- this.commandGateway.process(new DisburseCommand(productIdentifier, caseIdentifier));
+ this.commandGateway.process(new DisburseCommand(productIdentifier, caseIdentifier, command));
break;
case ACCEPT_PAYMENT:
- this.commandGateway.process(new AcceptPaymentCommand(productIdentifier, caseIdentifier));
+ this.commandGateway.process(new AcceptPaymentCommand(productIdentifier, caseIdentifier, command));
break;
case WRITE_OFF:
- this.commandGateway.process(new WriteOffCommand(productIdentifier, caseIdentifier));
+ this.commandGateway.process(new WriteOffCommand(productIdentifier, caseIdentifier, command));
break;
case CLOSE:
- this.commandGateway.process(new CloseCommand(productIdentifier, caseIdentifier));
+ this.commandGateway.process(new CloseCommand(productIdentifier, caseIdentifier, command));
break;
case RECOVER:
- this.commandGateway.process(new RecoverCommand(productIdentifier, caseIdentifier));
+ this.commandGateway.process(new RecoverCommand(productIdentifier, caseIdentifier, command));
break;
default:
- throw ServiceException.badRequest("Action ''{0}'' cannot be taken from current state.", actionIdentifier);
+ throw ServiceException.badRequest("Action ''{0}'' is not implemented for individual loans.", actionIdentifier);
}
}
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/AcceptPaymentCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/AcceptPaymentCommand.java
index 00916ff..e7c8fd9 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/AcceptPaymentCommand.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/AcceptPaymentCommand.java
@@ -15,16 +15,20 @@
*/
package io.mifos.individuallending.internal.command;
+import io.mifos.portfolio.api.v1.domain.Command;
+
/**
* @author Myrle Krantz
*/
public class AcceptPaymentCommand {
private final String productIdentifier;
private final String caseIdentifier;
+ private final Command command;
- public AcceptPaymentCommand(final String productIdentifier, final String caseIdentifier) {
+ public AcceptPaymentCommand(final String productIdentifier, final String caseIdentifier, final Command command) {
this.productIdentifier = productIdentifier;
this.caseIdentifier = caseIdentifier;
+ this.command = command;
}
public String getProductIdentifier() {
@@ -35,6 +39,10 @@
return caseIdentifier;
}
+ public Command getCommand() {
+ return command;
+ }
+
@Override
public String toString() {
return "AcceptPaymentCommand{" +
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/ApproveCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/ApproveCommand.java
index 25b2613..4765cf2 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/ApproveCommand.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/ApproveCommand.java
@@ -15,16 +15,20 @@
*/
package io.mifos.individuallending.internal.command;
+import io.mifos.portfolio.api.v1.domain.Command;
+
/**
* @author Myrle Krantz
*/
public class ApproveCommand {
private final String productIdentifier;
private final String caseIdentifier;
+ private final Command command;
- public ApproveCommand(final String productIdentifier, final String caseIdentifier) {
+ public ApproveCommand(final String productIdentifier, final String caseIdentifier, final Command command) {
this.productIdentifier = productIdentifier;
this.caseIdentifier = caseIdentifier;
+ this.command = command;
}
public String getProductIdentifier() {
@@ -35,6 +39,10 @@
return caseIdentifier;
}
+ public Command getCommand() {
+ return command;
+ }
+
@Override
public String toString() {
return "ApproveCommand{" +
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/CloseCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/CloseCommand.java
index 3829eeb..9fb578f 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/CloseCommand.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/CloseCommand.java
@@ -15,16 +15,20 @@
*/
package io.mifos.individuallending.internal.command;
+import io.mifos.portfolio.api.v1.domain.Command;
+
/**
* @author Myrle Krantz
*/
public class CloseCommand {
private final String productIdentifier;
private final String caseIdentifier;
+ private final Command command;
- public CloseCommand(final String productIdentifier, final String caseIdentifier) {
+ public CloseCommand(final String productIdentifier, final String caseIdentifier, final Command command) {
this.productIdentifier = productIdentifier;
this.caseIdentifier = caseIdentifier;
+ this.command = command;
}
public String getProductIdentifier() {
@@ -35,6 +39,10 @@
return caseIdentifier;
}
+ public Command getCommand() {
+ return command;
+ }
+
@Override
public String toString() {
return "CloseCommand{" +
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/DenyCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/DenyCommand.java
index 1592e21..f01dae9 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/DenyCommand.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/DenyCommand.java
@@ -15,16 +15,20 @@
*/
package io.mifos.individuallending.internal.command;
+import io.mifos.portfolio.api.v1.domain.Command;
+
/**
* @author Myrle Krantz
*/
public class DenyCommand {
private final String productIdentifier;
private final String caseIdentifier;
+ private final Command command;
- public DenyCommand(final String productIdentifier, final String caseIdentifier) {
+ public DenyCommand(final String productIdentifier, final String caseIdentifier, final Command command) {
this.productIdentifier = productIdentifier;
this.caseIdentifier = caseIdentifier;
+ this.command = command;
}
public String getProductIdentifier() {
@@ -35,6 +39,10 @@
return caseIdentifier;
}
+ public Command getCommand() {
+ return command;
+ }
+
@Override
public String toString() {
return "DenyCommand{" +
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/DisburseCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/DisburseCommand.java
index c251232..5356c4c 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/DisburseCommand.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/DisburseCommand.java
@@ -15,16 +15,20 @@
*/
package io.mifos.individuallending.internal.command;
+import io.mifos.portfolio.api.v1.domain.Command;
+
/**
* @author Myrle Krantz
*/
public class DisburseCommand {
private final String productIdentifier;
private final String caseIdentifier;
+ private final Command command;
- public DisburseCommand(final String productIdentifier, final String caseIdentifier) {
+ public DisburseCommand(final String productIdentifier, final String caseIdentifier, final Command command) {
this.productIdentifier = productIdentifier;
this.caseIdentifier = caseIdentifier;
+ this.command = command;
}
public String getProductIdentifier() {
@@ -35,6 +39,10 @@
return caseIdentifier;
}
+ public Command getCommand() {
+ return command;
+ }
+
@Override
public String toString() {
return "DisburseCommand{" +
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/OpenCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/OpenCommand.java
index 1330537..4ebd01d 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/OpenCommand.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/OpenCommand.java
@@ -15,16 +15,20 @@
*/
package io.mifos.individuallending.internal.command;
+import io.mifos.portfolio.api.v1.domain.Command;
+
/**
* @author Myrle Krantz
*/
public class OpenCommand {
private final String productIdentifier;
private final String caseIdentifier;
+ private final Command command;
- public OpenCommand(final String productIdentifier, final String caseIdentifier) {
+ public OpenCommand(final String productIdentifier, final String caseIdentifier, final Command command) {
this.productIdentifier = productIdentifier;
this.caseIdentifier = caseIdentifier;
+ this.command = command;
}
public String getProductIdentifier() {
@@ -35,6 +39,10 @@
return caseIdentifier;
}
+ public Command getCommand() {
+ return command;
+ }
+
@Override
public String toString() {
return "OpenCommand{" +
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/RecoverCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/RecoverCommand.java
index 2eecd8c..7c333ef 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/RecoverCommand.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/RecoverCommand.java
@@ -15,16 +15,20 @@
*/
package io.mifos.individuallending.internal.command;
+import io.mifos.portfolio.api.v1.domain.Command;
+
/**
* @author Myrle Krantz
*/
public class RecoverCommand {
private final String productIdentifier;
private final String caseIdentifier;
+ private final Command command;
- public RecoverCommand(final String productIdentifier, final String caseIdentifier) {
+ public RecoverCommand(final String productIdentifier, final String caseIdentifier, final Command command) {
this.productIdentifier = productIdentifier;
this.caseIdentifier = caseIdentifier;
+ this.command = command;
}
public String getProductIdentifier() {
@@ -35,6 +39,10 @@
return caseIdentifier;
}
+ public Command getCommand() {
+ return command;
+ }
+
@Override
public String toString() {
return "RecoverCommand{" +
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/WriteOffCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/WriteOffCommand.java
index 3859035..1f494c9 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/WriteOffCommand.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/WriteOffCommand.java
@@ -15,16 +15,20 @@
*/
package io.mifos.individuallending.internal.command;
+import io.mifos.portfolio.api.v1.domain.Command;
+
/**
* @author Myrle Krantz
*/
public class WriteOffCommand {
private final String productIdentifier;
private final String caseIdentifier;
+ private final Command command;
- public WriteOffCommand(final String productIdentifier, final String caseIdentifier) {
+ public WriteOffCommand(final String productIdentifier, final String caseIdentifier, final Command command) {
this.productIdentifier = productIdentifier;
this.caseIdentifier = caseIdentifier;
+ this.command = command;
}
public String getProductIdentifier() {
@@ -35,6 +39,10 @@
return caseIdentifier;
}
+ public Command getCommand() {
+ return command;
+ }
+
@Override
public String toString() {
return "WriteOffCommand{" +
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
index c72e65e..4d42435 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
@@ -16,23 +16,39 @@
package io.mifos.individuallending.internal.command.handler;
+import io.mifos.core.command.annotation.Aggregate;
+import io.mifos.core.command.annotation.CommandHandler;
import io.mifos.core.command.annotation.CommandLogLevel;
+import io.mifos.core.command.annotation.EventEmitter;
+import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.IndividualLendingPatternFactory;
+import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
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.individuallending.IndividualLendingPatternFactory;
import io.mifos.individuallending.internal.command.*;
+import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
+import io.mifos.individuallending.internal.repository.CaseParametersRepository;
+import io.mifos.individuallending.internal.service.IndividualLoanService;
+import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.events.EventConstants;
-import io.mifos.portfolio.service.internal.repository.CaseEntity;
-import io.mifos.portfolio.service.internal.repository.CaseRepository;
-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.mapper.CaseMapper;
+import io.mifos.portfolio.service.internal.mapper.ProductMapper;
+import io.mifos.portfolio.service.internal.repository.*;
+import io.mifos.portfolio.service.internal.util.AccountingAdapter;
+import io.mifos.portfolio.service.internal.util.ChargeInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
/**
* @author Myrle Krantz
*/
@@ -40,24 +56,86 @@
@Aggregate
public class IndividualLoanCommandHandler {
+ private final ProductRepository productRepository;
private final CaseRepository caseRepository;
+ private final CaseParametersRepository caseParametersRepository;
+ private final AccountingAdapter accountingAdapter;
+ private final IndividualLoanService individualLoanService;
@Autowired
- public IndividualLoanCommandHandler(final CaseRepository caseRepository) {
+ public IndividualLoanCommandHandler(final ProductRepository productRepository,
+ final CaseRepository caseRepository,
+ final CaseParametersRepository caseParametersRepository,
+ final AccountingAdapter accountingAdapter,
+ final IndividualLoanService individualLoanService) {
+ this.productRepository = productRepository;
this.caseRepository = caseRepository;
+ this.caseParametersRepository = caseParametersRepository;
+ this.accountingAdapter = accountingAdapter;
+ this.individualLoanService = individualLoanService;
}
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final OpenCommand command) {
+ final ProductEntity product = getProductOrThrow(command.getProductIdentifier());
final CaseEntity customerCase = getCaseOrThrow(command.getProductIdentifier(), command.getCaseIdentifier());
checkActionCanBeExecuted(Case.State.valueOf(customerCase.getCurrentState()), Action.OPEN);
+
+ final CaseParameters caseParameters =
+ caseParametersRepository.findByCaseId(customerCase.getId())
+ .map(CaseParametersMapper::mapEntity)
+ .orElseThrow(() -> ServiceException.notFound(
+ "Individual loan with identifier ''{0}''.''{1}'' doesn''t exist.",
+ command.getProductIdentifier(), command.getCaseIdentifier()));
+
+ final Set<ProductAccountAssignmentEntity> productAccountAssignments = product.getAccountAssignments();
+ final Set<CaseAccountAssignmentEntity> caseAccountAssignments = customerCase.getAccountAssignments();
+
+ final List<ChargeInstance> chargesNamedViaAccountDesignators =
+ individualLoanService.getChargeInstances(command.getProductIdentifier(), caseParameters, BigDecimal.ZERO, Action.OPEN, today(), LocalDate.now());
+ final List<ChargeInstance> chargesNamedViaAccountIdentifier = chargesNamedViaAccountDesignators.stream().map(x -> new ChargeInstance(
+ designatorToAccountIdentifierOrThrow(x.getFromAccount(), command.getCommand().getOneTimeAccountAssignments(), caseAccountAssignments, productAccountAssignments),
+ designatorToAccountIdentifierOrThrow(x.getToAccount(), command.getCommand().getOneTimeAccountAssignments(), caseAccountAssignments, productAccountAssignments),
+ x.getAmount())).collect(Collectors.toList());
+ //TODO: Accrual
+
+ accountingAdapter.bookCharges(chargesNamedViaAccountIdentifier,
+ command.getCommand().getNote(),
+ command.getProductIdentifier() + "." + command.getCaseIdentifier() + "." + Action.OPEN.name(),
+ Action.OPEN.getTransactionType());
+ //Only move to pending if book charges command was accepted.
updateCaseState(customerCase, Case.State.PENDING);
return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
}
+ private static LocalDate today() {
+ return LocalDate.now(ZoneId.of("UTC"));
+ }
+
+ private String designatorToAccountIdentifierOrThrow(final String accountDesignator,
+ final List<AccountAssignment> oneTimeAccountAssignments,
+ final Set<CaseAccountAssignmentEntity> caseAccountAssignments,
+ final Set<ProductAccountAssignmentEntity> productAccountAssignments) {
+ return allAccountAssignmentsAsStream(oneTimeAccountAssignments, caseAccountAssignments, productAccountAssignments)
+ .filter(x -> x.getDesignator().equals(accountDesignator))
+ .findFirst()
+ .map(AccountAssignment::getAccountIdentifier)
+ .orElseThrow(() -> ServiceException.badRequest("A required account designator was not set ''{0}''.", accountDesignator));
+ }
+
+ private Stream<AccountAssignment> allAccountAssignmentsAsStream(
+ final List<AccountAssignment> oneTimeAccountAssignments,
+ final Set<CaseAccountAssignmentEntity> caseAccountAssignments,
+ final Set<ProductAccountAssignmentEntity> productAccountAssignments) {
+ return Stream.concat(Stream.concat(
+ oneTimeAccountAssignments.stream(),
+ caseAccountAssignments.stream().map(CaseMapper::mapAccountAssignmentEntity)),
+ productAccountAssignments.stream().map(ProductMapper::mapAccountAssignmentEntity));
+ }
+
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.DENY_INDIVIDUALLOAN_CASE)
@@ -130,7 +208,12 @@
private CaseEntity getCaseOrThrow(final String productIdentifier, final String caseIdentifier) {
return caseRepository.findByProductIdentifierAndIdentifier(productIdentifier, caseIdentifier)
- .orElseThrow(() -> ServiceException.notFound("case not found {0}.{1}", productIdentifier, caseIdentifier));
+ .orElseThrow(() -> ServiceException.notFound("Case not found ''{0}.{1}''.", productIdentifier, caseIdentifier));
+ }
+
+ private ProductEntity getProductOrThrow(final String productIdentifier) {
+ return productRepository.findByIdentifier(productIdentifier)
+ .orElseThrow(() -> ServiceException.notFound("Product not found ''{0}''.", productIdentifier));
}
private void checkActionCanBeExecuted(final Case.State state, final Action action) {
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 a10a100..e3ec87b 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
@@ -27,6 +27,7 @@
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.Product;
+import io.mifos.portfolio.service.internal.util.ChargeInstance;
import org.javamoney.calc.common.Rate;
import org.javamoney.moneta.Money;
import org.springframework.beans.factory.annotation.Autowired;
@@ -80,7 +81,7 @@
.orElseThrow(() -> new IllegalArgumentException("Non-existent product identifier."));
final int minorCurrencyUnitDigits = product.getMinorCurrencyUnitDigits();
- final List<ScheduledAction> scheduledActions = scheduledActionService.getScheduledActions(initialDisbursalDate, caseParameters);
+ final List<ScheduledAction> scheduledActions = scheduledActionService.getHypotheticalScheduledActions(initialDisbursalDate, caseParameters);
final List<ScheduledCharge> scheduledCharges = getScheduledCharges(productIdentifier, minorCurrencyUnitDigits, caseParameters.getMaximumBalance(), scheduledActions);
@@ -90,6 +91,14 @@
.map(IndividualLoanService::chargeNameFromChargeDefinition)
.collect(Collectors.toSet());
+ return constructPage(pageIndex, size, plannedPaymentsElements, chargeNames);
+ }
+
+ private static PlannedPaymentPage constructPage(
+ final int pageIndex,
+ final int size,
+ final List<PlannedPayment> plannedPaymentsElements,
+ final Set<ChargeName> chargeNames) {
final int fromIndex = size*pageIndex;
final int toIndex = Math.min(size*(pageIndex+1), plannedPaymentsElements.size());
final List<PlannedPayment> elements = plannedPaymentsElements.subList(fromIndex, toIndex);
@@ -104,6 +113,29 @@
return ret;
}
+ public List<ChargeInstance> getChargeInstances(final String productIdentifier,
+ final CaseParameters caseParameters,
+ final BigDecimal currentBalance,
+ final Action action,
+ final LocalDate initialDisbursalDate,
+ final LocalDate forDate) {
+ final Product product = productService.findByIdentifier(productIdentifier)
+ .orElseThrow(() -> new IllegalArgumentException("Non-existent product identifier."));
+ final int minorCurrencyUnitDigits = product.getMinorCurrencyUnitDigits();
+ final List<ScheduledAction> scheduledActions = scheduledActionService.getScheduledActions(initialDisbursalDate, caseParameters, action, forDate);
+ final List<ScheduledCharge> scheduledCharges = getScheduledCharges(productIdentifier, minorCurrencyUnitDigits, currentBalance, scheduledActions);
+
+ final CostComponentsForRepaymentPeriod costComponentsForScheduledCharges = getCostComponentsForScheduledCharges(scheduledCharges, currentBalance, minorCurrencyUnitDigits);
+
+ return costComponentsForScheduledCharges.costComponents.entrySet().stream()
+ .map(IndividualLoanService::mapToChargeInstance)
+ .collect(Collectors.toList());
+ }
+
+ private static ChargeInstance mapToChargeInstance(final Map.Entry<ChargeDefinition, CostComponent> x) {
+ return new ChargeInstance(x.getKey().getFromAccountDesignator(), x.getKey().getToAccountDesignator(), x.getValue().getAmount());
+ }
+
private static ChargeName chargeNameFromChargeDefinition(final ScheduledCharge scheduledCharge) {
return new ChargeName(scheduledCharge.getChargeDefinition().getIdentifier(), scheduledCharge.getChargeDefinition().getName());
}
@@ -126,12 +158,17 @@
chargeDefinitionsMappedByChargeAction,
chargeDefinitionsMappedByAccrueAction,
acceptPaymentDefinition);
- int digitsInInitialeBalance = initialBalance.precision();
- final Map<Period, BigDecimal> ratesByPeriod = periodChargeCalculator.getPeriodRates(scheduledCharges, digitsInInitialeBalance + minorCurrencyUnitDigits + EXTRA_PRECISION);
+ int digitsInInitialBalance = initialBalance.precision();
+ final Map<Period, BigDecimal> accrualRatesByPeriod
+ = periodChargeCalculator.getPeriodAccrualRates(scheduledCharges,
+ digitsInInitialBalance + minorCurrencyUnitDigits + EXTRA_PRECISION);
- final BigDecimal geometricMean = ratesByPeriod.values().stream().collect(RateCollectors.geometricMean(digitsInInitialeBalance + minorCurrencyUnitDigits + EXTRA_PRECISION));
-
- acceptPaymentDefinition.setAmount(loanPayment(initialBalance, ratesByPeriod.size(), geometricMean));
+ if (accrualRatesByPeriod.size() != 0) {
+ final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream().collect(RateCollectors.geometricMean(digitsInInitialBalance + minorCurrencyUnitDigits + EXTRA_PRECISION));
+ acceptPaymentDefinition.setAmount(loanPaymentInContextOfAccruedInterest(initialBalance, accrualRatesByPeriod.size(), geometricMeanAccrualRate));
+ }
+ else
+ acceptPaymentDefinition.setAmount(initialBalance);
return scheduledCharges;
}
@@ -182,29 +219,14 @@
final List<PlannedPayment> plannedPayments = new ArrayList<>();
for (final Period repaymentPeriod : sortedRepaymentPeriods)
{
- final Map<String, CostComponent> costComponentMap = new HashMap<>();
final SortedSet<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
- for (final ScheduledCharge scheduledCharge : scheduledChargesInPeriod)
- {
- final CostComponent costComponent = costComponentMap
- .computeIfAbsent(scheduledCharge.getChargeDefinition().getIdentifier(),
- chargeIdentifier -> {
- final CostComponent ret = new CostComponent();
- ret.setChargeIdentifier(scheduledCharge.getChargeDefinition().getIdentifier());
- ret.setAmount(BigDecimal.ZERO);
- return ret;
- });
+ final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ getCostComponentsForScheduledCharges(scheduledChargesInPeriod, balance, minorCurrencyUnitDigits);
- final BigDecimal chargeAmount = howToApplyScheduledChargeToBalance(scheduledCharge, 8)
- .apply(balance)
- .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
- if (chargeDefinitionTouchesCustomerLoanAccount(scheduledCharge.getChargeDefinition()))
- balance = balance.add(chargeAmount);
- costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
- }
final PlannedPayment plannedPayment = new PlannedPayment();
- plannedPayment.setCostComponents(costComponentMap.values().stream().collect(Collectors.toList()));
+ plannedPayment.setCostComponents(costComponentsForRepaymentPeriod.costComponents.values().stream().collect(Collectors.toList()));
plannedPayment.setDate(DateConverter.toIsoString(repaymentPeriod.getEndDate()));
+ balance = balance.add(costComponentsForRepaymentPeriod.balanceAdjustment);
plannedPayment.setRemainingPrincipal(balance);
plannedPayments.add(plannedPayment);
}
@@ -221,6 +243,49 @@
return plannedPayments;
}
+ private static class CostComponentsForRepaymentPeriod {
+ final Map<ChargeDefinition, CostComponent> costComponents;
+ final BigDecimal balanceAdjustment;
+
+ private CostComponentsForRepaymentPeriod(
+ final Map<ChargeDefinition, CostComponent> costComponents,
+ final BigDecimal balanceAdjustment) {
+ this.costComponents = costComponents;
+ this.balanceAdjustment = balanceAdjustment;
+ }
+ }
+
+ static private CostComponentsForRepaymentPeriod getCostComponentsForScheduledCharges(
+ final Collection<ScheduledCharge> scheduledCharges,
+ final BigDecimal balance,
+ final int minorCurrencyUnitDigits) {
+ BigDecimal balanceAdjustment = BigDecimal.ZERO;
+
+ final Map<ChargeDefinition, CostComponent> costComponentMap = new HashMap<>();
+ for (final ScheduledCharge scheduledCharge : scheduledCharges)
+ {
+ final CostComponent costComponent = costComponentMap
+ .computeIfAbsent(scheduledCharge.getChargeDefinition(),
+ chargeIdentifier -> {
+ final CostComponent ret = new CostComponent();
+ ret.setChargeIdentifier(scheduledCharge.getChargeDefinition().getIdentifier());
+ ret.setAmount(BigDecimal.ZERO);
+ return ret;
+ });
+
+ final BigDecimal chargeAmount = howToApplyScheduledChargeToBalance(scheduledCharge, 8)
+ .apply(balance)
+ .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
+ if (chargeDefinitionTouchesCustomerLoanAccount(scheduledCharge.getChargeDefinition()))
+ balanceAdjustment = balanceAdjustment.add(chargeAmount);
+ costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
+ }
+
+ return new CostComponentsForRepaymentPeriod(
+ costComponentMap,
+ balanceAdjustment);
+ }
+
private static boolean chargeDefinitionTouchesCustomerLoanAccount(final ChargeDefinition chargeDefinition)
{
return chargeDefinition.getToAccountDesignator().equals(AccountDesignators.CUSTOMER_LOAN) ||
@@ -243,14 +308,14 @@
}
}
- private BigDecimal loanPayment(
+ private BigDecimal loanPaymentInContextOfAccruedInterest(
final BigDecimal initialBalance,
final int periodCount,
- final BigDecimal geometricMean) {
+ final BigDecimal geometricMeanOfInterest) {
if (periodCount == 0)
- throw new IllegalStateException();
+ throw new IllegalStateException("To calculate a loan payment there must be at least one payment period.");
- final MonetaryAmount presentValue = AnnuityPayment.calculate(Money.of(initialBalance, "XXX"), Rate.of(geometricMean), periodCount);
+ final MonetaryAmount presentValue = AnnuityPayment.calculate(Money.of(initialBalance, "XXX"), Rate.of(geometricMeanOfInterest), periodCount);
return BigDecimal.valueOf(presentValue.getNumber().doubleValueExact()).negate();
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/Period.java b/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
index e9ec4bb..aa4f170 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
@@ -28,9 +28,9 @@
final private LocalDate beginDate;
final private LocalDate endDate;
- Period(final LocalDate beginDate, final LocalDate endDate) {
+ Period(final LocalDate beginDate, final LocalDate endDateExclusive) {
this.beginDate = beginDate;
- this.endDate = endDate;
+ this.endDate = endDateExclusive;
}
Period(final LocalDate beginDate, final int periodLength) {
@@ -51,6 +51,10 @@
return ChronoUnit.DAYS.getDuration().multipliedBy(days);
}
+ boolean containsDate(final LocalDate date) {
+ return this.getBeginDate().compareTo(date) <= 0 && this.getEndDate().compareTo(date) > 0;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
index decafe2..3466c21 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
@@ -34,7 +34,7 @@
{
}
- Map<Period, BigDecimal> getPeriodRates(final List<ScheduledCharge> scheduledCharges, final int precision) {
+ Map<Period, BigDecimal> getPeriodAccrualRates(final List<ScheduledCharge> scheduledCharges, final int precision) {
return scheduledCharges.stream()
.filter(PeriodChargeCalculator::accruedCharge)
.collect(Collectors.groupingBy(scheduledCharge -> scheduledCharge.getScheduledAction().repaymentPeriod,
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionService.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionService.java
index 19fe462..0e3c3f6 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionService.java
@@ -38,8 +38,14 @@
@Service
public class ScheduledActionService {
- List<ScheduledAction> getScheduledActions(final @Nonnull LocalDate initialDisbursalDate,
- final @Nonnull CaseParameters caseParameters)
+ List<ScheduledAction> getHypotheticalScheduledActions(final @Nonnull LocalDate initialDisbursalDate,
+ final @Nonnull CaseParameters caseParameters)
+ {
+ return getHypotheticalScheduledActionsHelper(initialDisbursalDate, caseParameters).collect(Collectors.toList());
+ }
+
+ private Stream<ScheduledAction> getHypotheticalScheduledActionsHelper(final @Nonnull LocalDate initialDisbursalDate,
+ final @Nonnull CaseParameters caseParameters)
{
//'Rough' end date, because if the repayment period takes the last period after that end date, then the repayment
// period will 'win'.
@@ -49,10 +55,11 @@
final Period firstPeriod = repaymentPeriods.first();
final Period lastPeriod = repaymentPeriods.last();
- return Stream.concat(Stream.of(new ScheduledAction(Action.APPROVE, initialDisbursalDate, firstPeriod, firstPeriod)),
+ return Stream.concat(Stream.of(
+ new ScheduledAction(Action.OPEN, initialDisbursalDate, firstPeriod, firstPeriod),
+ new ScheduledAction(Action.APPROVE, initialDisbursalDate, firstPeriod, firstPeriod)),
Stream.concat(repaymentPeriods.stream().flatMap(this::generateScheduledActionsForRepaymentPeriod),
- Stream.of(new ScheduledAction(Action.CLOSE, lastPeriod.getEndDate(), lastPeriod, lastPeriod))))
- .collect(Collectors.toList());
+ Stream.of(new ScheduledAction(Action.CLOSE, lastPeriod.getEndDate(), lastPeriod, lastPeriod))));
}
private LocalDate getEndDate(final @Nonnull CaseParameters caseParameters,
@@ -238,4 +245,14 @@
final int maxDay = YearMonth.of(paymentDate.getYear(), paymentDate.getMonth()).lengthOfMonth()-1;
return paymentDate.plusDays(Math.min(maxDay, alignmentDay));
}
+
+ public List<ScheduledAction> getScheduledActions(final @Nonnull LocalDate initialDisbursalDate,
+ final CaseParameters caseParameters,
+ final Action action,
+ final LocalDate time) {
+ return getHypotheticalScheduledActionsHelper(initialDisbursalDate, caseParameters)
+ .filter(x -> x.actionPeriod.containsDate(time))
+ .filter(x -> x.action.equals(action))
+ .collect(Collectors.toList());
+ }
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/InitializeCommandHandler.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/InitializeCommandHandler.java
index 93299b6..ab3eff1 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/InitializeCommandHandler.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/InitializeCommandHandler.java
@@ -15,18 +15,15 @@
*/
package io.mifos.portfolio.service.internal.command.handler;
-import io.mifos.core.command.annotation.CommandLogLevel;
-import io.mifos.portfolio.api.v1.events.EventConstants;
-import io.mifos.portfolio.service.ServiceConstants;
-import io.mifos.portfolio.service.internal.command.InitializeServiceCommand;
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.mariadb.domain.FlywayFactoryBean;
+import io.mifos.portfolio.api.v1.events.EventConstants;
+import io.mifos.portfolio.service.internal.command.InitializeServiceCommand;
import io.mifos.portfolio.service.internal.util.RhythmAdapter;
-import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
import javax.sql.DataSource;
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/CaseMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/CaseMapper.java
index bc966d7..10c8452 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/CaseMapper.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/CaseMapper.java
@@ -48,7 +48,7 @@
return ret;
}
- private static AccountAssignment mapAccountAssignmentEntity(final CaseAccountAssignmentEntity instance) {
+ public static AccountAssignment mapAccountAssignmentEntity(final CaseAccountAssignmentEntity instance) {
final AccountAssignment ret = new AccountAssignment();
ret.setDesignator(instance.getDesignator());
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ProductMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ProductMapper.java
index d0fa6ed..27389f1 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ProductMapper.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ProductMapper.java
@@ -50,7 +50,7 @@
product.setCurrencyCode(productEntity.getCurrencyCode());
product.setMinorCurrencyUnitDigits(productEntity.getMinorCurrencyUnitDigits());
product.setAccountAssignments(productEntity.getAccountAssignments()
- .stream().map(ProductMapper::map).collect(Collectors.toSet()));
+ .stream().map(ProductMapper::mapAccountAssignmentEntity).collect(Collectors.toSet()));
product.setParameters(productEntity.getParameters());
product.setCreatedBy(productEntity.getCreatedBy());
product.setCreatedOn(DateConverter.toIsoString(productEntity.getCreatedOn()));
@@ -111,7 +111,7 @@
return ret;
}
- private static AccountAssignment map (final ProductAccountAssignmentEntity productAccountAssignmentEntity)
+ public static AccountAssignment mapAccountAssignmentEntity (final ProductAccountAssignmentEntity productAccountAssignmentEntity)
{
final AccountAssignment ret = new AccountAssignment();
ret.setDesignator(productAccountAssignmentEntity.getDesignator());
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
index c27be3e..11368c1 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
@@ -18,6 +18,8 @@
import io.mifos.core.lang.ServiceException;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.CasePage;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
import io.mifos.portfolio.service.internal.mapper.CaseMapper;
import io.mifos.portfolio.service.internal.pattern.PatternFactoryRegistry;
import io.mifos.portfolio.service.internal.repository.CaseEntity;
@@ -33,10 +35,7 @@
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
+import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -48,15 +47,18 @@
private final PatternFactoryRegistry patternFactoryRegistry;
private final ProductRepository productRepository;
private final CaseRepository caseRepository;
+ private final ChargeDefinitionService chargeDefinitionService;
@Autowired
public CaseService(
final PatternFactoryRegistry patternFactoryRegistry,
final ProductRepository productRepository,
- final CaseRepository caseRepository) {
+ final CaseRepository caseRepository,
+ final ChargeDefinitionService chargeDefinitionService) {
this.patternFactoryRegistry = patternFactoryRegistry;
this.productRepository = productRepository;
this.caseRepository = caseRepository;
+ this.chargeDefinitionService = chargeDefinitionService;
}
public CasePage findAllEntities(final String productIdentifier,
@@ -121,4 +123,18 @@
public boolean existsByProductIdentifier(final String productIdentifier) {
return caseRepository.existsByProductIdentifier(productIdentifier);
}
+
+ public List<CostComponent> getActionCostComponentsForCase(final String productIdentifier,
+ final String caseIdentifier,
+ final String actionIdentifier) {
+ final Map<String, List<ChargeDefinition>> chargeDefinitions = chargeDefinitionService.getChargeDefinitionsMappedByChargeAction(productIdentifier);
+ final List<ChargeDefinition> chargeDefinitionsForAction = chargeDefinitions.get(actionIdentifier);
+ return chargeDefinitionsForAction.stream().map(x -> {
+ final CostComponent ret = new CostComponent();
+ ret.setChargeIdentifier(x.getIdentifier());
+ ret.setAmount(x.getAmount()); //TODO: This is too simplistic. Will only work for fixed charges and no accrual.
+ return ret;
+ }).collect(Collectors.toList());
+
+ }
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
index 9d33487..f802c51 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
@@ -18,17 +18,29 @@
import io.mifos.accounting.api.v1.client.AccountNotFoundException;
import io.mifos.accounting.api.v1.client.LedgerManager;
import io.mifos.accounting.api.v1.client.LedgerNotFoundException;
+import io.mifos.accounting.api.v1.domain.Account;
+import io.mifos.accounting.api.v1.domain.Creditor;
+import io.mifos.accounting.api.v1.domain.Debtor;
+import io.mifos.accounting.api.v1.domain.JournalEntry;
+import io.mifos.core.api.util.UserContextHolder;
+import io.mifos.core.lang.DateConverter;
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import org.apache.commons.lang.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.ENTRY;
+
/**
* @author Myrle Krantz
*/
@@ -44,6 +56,59 @@
this.ledgerManager = ledgerManager;
}
+ public void bookCharges(final List<ChargeInstance> costComponents,
+ final String note,
+ final String message,
+ final String transactionType) {
+ final Set<Creditor> creditors = costComponents.stream()
+ .map(AccountingAdapter::mapToCreditor)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toSet());
+ final Set<Debtor> debtors = costComponents.stream()
+ .map(AccountingAdapter::mapToDebtor)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toSet());
+
+ final JournalEntry journalEntry = new JournalEntry();
+ journalEntry.setCreditors(creditors);
+ journalEntry.setDebtors(debtors);
+ journalEntry.setClerk(UserContextHolder.checkedGetUser());
+ journalEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now()));
+ journalEntry.setMessage(message);
+ journalEntry.setTransactionType(transactionType);
+ journalEntry.setNote(note);
+ journalEntry.setTransactionIdentifier("bastet" + RandomStringUtils.random(26, true, true));
+
+ ledgerManager.createJournalEntry(journalEntry);
+ }
+
+ private static Optional<Debtor> mapToDebtor(final ChargeInstance chargeInstance) {
+ if (chargeInstance.getAmount().compareTo(BigDecimal.ZERO) == 0)
+ return Optional.empty();
+
+ final Debtor ret = new Debtor();
+ ret.setAccountNumber(chargeInstance.getFromAccount());
+ ret.setAmount(chargeInstance.getAmount().toPlainString());
+ return Optional.of(ret);
+ }
+
+ private static Optional<Creditor> mapToCreditor(final ChargeInstance chargeInstance) {
+ if (chargeInstance.getAmount().compareTo(BigDecimal.ZERO) == 0)
+ return Optional.empty();
+
+ final Creditor ret = new Creditor();
+ ret.setAccountNumber(chargeInstance.getToAccount());
+ ret.setAmount(chargeInstance.getAmount().toPlainString());
+ return Optional.of(ret);
+ }
+
+ public BigDecimal getCurrentBalance(final String accountIdentifier) {
+ final Account account = ledgerManager.findAccount(accountIdentifier);
+ return BigDecimal.valueOf(account.getBalance());
+ }
+
public static boolean accountAssignmentsCoverChargeDefinitions(
final Set<AccountAssignment> accountAssignments,
@@ -57,11 +122,26 @@
public static Set<String> getRequiredAccountDesignators(final Collection<ChargeDefinition> chargeDefinitionEntities) {
return chargeDefinitionEntities.stream()
- .flatMap(x -> Stream.of(x.getAccrualAccountDesignator(), x.getFromAccountDesignator(), x.getToAccountDesignator()))
+ .flatMap(AccountingAdapter::getAutomaticActionAccountDesignators)
.filter(x -> x != null)
.collect(Collectors.toSet());
}
+ private static Stream<String> getAutomaticActionAccountDesignators(final ChargeDefinition chargeDefinition) {
+ final Stream.Builder<String> retBuilder = Stream.builder();
+
+ checkAddDesignator(chargeDefinition.getFromAccountDesignator(), retBuilder);
+ checkAddDesignator(chargeDefinition.getAccrualAccountDesignator(), retBuilder);
+ checkAddDesignator(chargeDefinition.getToAccountDesignator(), retBuilder);
+
+ return retBuilder.build();
+ }
+
+ private static void checkAddDesignator(final String accountDesignator, final Stream.Builder<String> retBuilder) {
+ if (accountDesignator != null && !accountDesignator.equals(ENTRY))
+ retBuilder.add(accountDesignator);
+ }
+
public boolean accountAssignmentsRepresentRealAccounts(final Set<AccountAssignment> accountAssignments)
{
return accountAssignments.stream().allMatch(this::accountAssignmentRepresentsRealAccount);
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/util/ChargeInstance.java b/service/src/main/java/io/mifos/portfolio/service/internal/util/ChargeInstance.java
new file mode 100644
index 0000000..0056c84
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/util/ChargeInstance.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.portfolio.service.internal.util;
+
+import java.math.BigDecimal;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+public class ChargeInstance {
+ private final String fromAccount;
+ private final String toAccount;
+ private final BigDecimal amount;
+
+ public ChargeInstance(final String fromAccount,
+ final String toAccount,
+ final BigDecimal amount) {
+ this.fromAccount = fromAccount;
+ this.toAccount = toAccount;
+ this.amount = amount;
+ }
+
+ public String getFromAccount() {
+ return fromAccount;
+ }
+
+ public String getToAccount() {
+ return toAccount;
+ }
+
+ public BigDecimal getAmount() {
+ return amount;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ChargeInstance that = (ChargeInstance) o;
+ return Objects.equals(fromAccount, that.fromAccount) &&
+ Objects.equals(toAccount, that.toAccount) &&
+ Objects.equals(amount, that.amount);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fromAccount, toAccount, amount);
+ }
+
+ @Override
+ public String toString() {
+ return "ChargeInstance{" +
+ "fromAccount='" + fromAccount + '\'' +
+ ", toAccount='" + toAccount + '\'' +
+ ", amount=" + amount +
+ '}';
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
index 22e5a75..8b0eeba 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
@@ -37,7 +37,6 @@
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
-import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -191,8 +190,7 @@
{
checkThatCaseExists(productIdentifier, caseIdentifier);
- return Collections.emptyList();
- //return caseService.getActionCostComponentsForCase(productIdentifier, caseIdentifier);
+ return caseService.getActionCostComponentsForCase(productIdentifier, caseIdentifier, actionIdentifier);
}
@Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CASE_MANAGEMENT)
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 59032e6..08f2c5c 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
@@ -15,7 +15,7 @@
*/
package io.mifos.individuallending.internal.service;
-import io.mifos.portfolio.api.v1.domain.*;
+import io.mifos.individuallending.IndividualLendingPatternFactory;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.caseinstance.ChargeName;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
@@ -23,9 +23,10 @@
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
-import io.mifos.individuallending.IndividualLendingPatternFactory;
+import io.mifos.portfolio.api.v1.domain.*;
import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
import io.mifos.portfolio.service.internal.service.ProductService;
+import io.mifos.portfolio.service.internal.util.ChargeInstance;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,11 +40,55 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.PROCESSING_FEE_ID;
+
/**
* @author Myrle Krantz
*/
@RunWith(Parameterized.class)
public class IndividualLoanServiceTest {
+
+ private static class ActionDatePair {
+ final Action action;
+ final LocalDate localDate;
+
+ ActionDatePair(final Action action, final LocalDate localDate) {
+ this.action = action;
+ this.localDate = localDate;
+ }
+
+ Action getAction() {
+ return action;
+ }
+
+ LocalDate getLocalDate() {
+ return localDate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ActionDatePair that = (ActionDatePair) o;
+ return action == that.action &&
+ Objects.equals(localDate, that.localDate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(action, localDate);
+ }
+
+ @Override
+ public String toString() {
+ return "ActionDatePair{" +
+ "action=" + action +
+ ", localDate=" + localDate +
+ '}';
+ }
+ }
+
+
private static class TestCase {
private final String description;
private String productIdentifier = "blah";
@@ -52,6 +97,7 @@
private LocalDate initialDisbursementDate;
private Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction;
private Set<String> expectedChargeIdentifiers = new HashSet<>(Arrays.asList(ChargeIdentifiers.INTEREST_ID, ChargeIdentifiers.PAYMENT_ID));
+ private Map<ActionDatePair, List<ChargeInstance>> chargeInstancesForActions = new HashMap<>();
TestCase(final String description) {
this.description = description;
@@ -82,6 +128,18 @@
return this;
}
+ TestCase expectAdditionalChargeIdentifier(final String newVal) {
+ this.expectedChargeIdentifiers.add(newVal);
+ return this;
+ }
+
+ TestCase expectChargeInstancesForActionDatePair(final Action action,
+ final LocalDate forDate,
+ final List<ChargeInstance> chargeInstances) {
+ this.chargeInstancesForActions.put(new ActionDatePair(action, forDate), chargeInstances);
+ return this;
+ }
+
@Override
public String toString() {
return "TestCase{" +
@@ -98,7 +156,10 @@
ret.add(chargeDefaultsCase());
return ret;
}
+
private final TestCase testCase;
+ private final IndividualLoanService testSubject;
+ private final Product product;
private static TestCase simpleCase()
@@ -113,12 +174,20 @@
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = new HashMap<>();
chargeDefinitionsMappedByAction.put(Action.APPLY_INTEREST.name(), getInterestChargeDefinition(0.01, ChronoUnit.YEARS));
+ chargeDefinitionsMappedByAction.put(Action.OPEN.name(),
+ getFixedSingleChargeDefinition(10.0, Action.OPEN, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME));
return new TestCase("simpleCase")
.minorCurrencyUnitDigits(2)
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
- .chargeDefinitionsMappedByAction(chargeDefinitionsMappedByAction);
+ .chargeDefinitionsMappedByAction(chargeDefinitionsMappedByAction)
+ .expectAdditionalChargeIdentifier(PROCESSING_FEE_ID)
+ .expectChargeInstancesForActionDatePair(Action.OPEN, initialDisbursementDate,
+ Collections.singletonList(new ChargeInstance(
+ AccountDesignators.ENTRY,
+ AccountDesignators.PROCESSING_FEE_INCOME,
+ BigDecimal.valueOf(10).setScale(2, BigDecimal.ROUND_UNNECESSARY))));
}
private static TestCase yearLoanTestCase()
@@ -159,7 +228,7 @@
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.chargeDefinitionsMappedByAction(chargeDefinitionsMappedByAction)
- .expectedChargeIdentifiers(new HashSet<>(Arrays.asList(ChargeIdentifiers.RETURN_DISBURSEMENT_ID, ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, ChargeIdentifiers.INTEREST_ID, ChargeIdentifiers.PAYMENT_ID)));
+ .expectedChargeIdentifiers(new HashSet<>(Arrays.asList(PROCESSING_FEE_ID, ChargeIdentifiers.RETURN_DISBURSEMENT_ID, ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, ChargeIdentifiers.INTEREST_ID, ChargeIdentifiers.PAYMENT_ID)));
}
private static List<ChargeDefinition> getInterestChargeDefinition(final double amount, final ChronoUnit forCycleSizeUnit) {
@@ -176,21 +245,39 @@
return Collections.singletonList(ret);
}
+ private static List<ChargeDefinition> getFixedSingleChargeDefinition(
+ final double amount,
+ final Action action,
+ final String chargeIdentifier,
+ final String feeAccountDesignator) {
+ final ChargeDefinition ret = new ChargeDefinition();
+ ret.setAmount(BigDecimal.valueOf(amount));
+ ret.setIdentifier(chargeIdentifier);
+ ret.setAccrueAction(null);
+ ret.setChargeAction(action.name());
+ ret.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+ ret.setFromAccountDesignator(AccountDesignators.ENTRY);
+ ret.setToAccountDesignator(feeAccountDesignator);
+ ret.setForCycleSizeUnit(null);
+ return Collections.singletonList(ret);
+ }
+
public IndividualLoanServiceTest(final TestCase testCase)
{
this.testCase = testCase;
- }
- @Test
- public void getPlannedPayments() throws Exception {
final ProductService productServiceMock = Mockito.mock(ProductService.class);
final ChargeDefinitionService chargeDefinitionServiceMock = Mockito.mock(ChargeDefinitionService.class);
- final Product product = new Product();
+ product = new Product();
product.setMinorCurrencyUnitDigits(testCase.minorCurrencyUnitDigits);
Mockito.doReturn(Optional.of(product)).when(productServiceMock).findByIdentifier(testCase.productIdentifier);
Mockito.doReturn(testCase.chargeDefinitionsMappedByAction).when(chargeDefinitionServiceMock).getChargeDefinitionsMappedByChargeAction(testCase.productIdentifier);
- final IndividualLoanService testSubject = new IndividualLoanService(productServiceMock, chargeDefinitionServiceMock, new ScheduledActionService(), new PeriodChargeCalculator());
+ testSubject = new IndividualLoanService(productServiceMock, chargeDefinitionServiceMock, new ScheduledActionService(), new PeriodChargeCalculator());
+ }
+
+ @Test
+ public void getPlannedPayments() throws Exception {
final PlannedPaymentPage firstPage = testSubject.getPlannedPaymentsPage(testCase.productIdentifier,
testCase.caseParameters,
0,
@@ -246,6 +333,19 @@
Assert.assertEquals(testCase.expectedChargeIdentifiers, resultChargeIdentifiers);
}
+ @Test
+ public void createChargeInstances() {
+ testCase.chargeInstancesForActions.entrySet().forEach(entry ->
+ Assert.assertEquals(
+ entry.getValue(),
+ testSubject.getChargeInstances(
+ testCase.productIdentifier,
+ testCase.caseParameters,
+ testCase.caseParameters.getMaximumBalance(),
+ entry.getKey().getAction(),
+ testCase.initialDisbursementDate, entry.getKey().getLocalDate())));
+ }
+
private double percentDifference(final BigDecimal maxPayment, final BigDecimal minPayment) {
final BigDecimal difference = maxPayment.subtract(minPayment);
final BigDecimal percentDifference = difference.divide(maxPayment, 4, BigDecimal.ROUND_UP);
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/PeriodChargeCalculatorTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/PeriodChargeCalculatorTest.java
index 3850062..8571638 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/PeriodChargeCalculatorTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/PeriodChargeCalculatorTest.java
@@ -154,7 +154,7 @@
public void test()
{
final PeriodChargeCalculator testSubject = new PeriodChargeCalculator();
- final Map<Period, BigDecimal> periodRates = testSubject.getPeriodRates(testCase.scheduledCharges, testCase.precision);
+ final Map<Period, BigDecimal> periodRates = testSubject.getPeriodAccrualRates(testCase.scheduledCharges, testCase.precision);
Assert.assertEquals(testCase.expectedPeriodRates, periodRates);
}
}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/PeriodTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/PeriodTest.java
new file mode 100644
index 0000000..0235e48
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/PeriodTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+
+/**
+ * @author Myrle Krantz
+ */
+public class PeriodTest {
+ private static LocalDate today;
+ private static LocalDate yesterday;
+ private static LocalDate tommorrow;
+ private static LocalDate dayAfterTommorrow;
+
+ @BeforeClass
+ public static void prepare() {
+ today = LocalDate.now(ZoneId.of("UTC"));
+ yesterday = today.minusDays(1);
+ tommorrow = today.plusDays(1);
+ dayAfterTommorrow = tommorrow.plusDays(1);
+ }
+
+ @Test
+ public void getDuration() throws Exception {
+ final Period testSubjectByDates = new Period(today, dayAfterTommorrow);
+ Assert.assertEquals(2, testSubjectByDates.getDuration().toDays());
+
+ final Period testSubjectByDuration = new Period(today, 5);
+ Assert.assertEquals(5, testSubjectByDuration.getDuration().toDays());
+ }
+
+ @Test
+ public void containsDate() throws Exception {
+ final Period testSubject = new Period(today, 1);
+
+ Assert.assertTrue(testSubject.containsDate(today));
+ Assert.assertFalse(testSubject.containsDate(tommorrow));
+ Assert.assertFalse(testSubject.containsDate(yesterday));
+ Assert.assertFalse(testSubject.containsDate(dayAfterTommorrow));
+
+ }
+
+ @Test
+ public void compareTo() throws Exception {
+ final Period yesterdayPeriod = new Period(yesterday, today);
+ final Period todayPeriod = new Period(today, tommorrow);
+ final Period tommorrowPeriod = new Period(tommorrow, dayAfterTommorrow);
+
+ Assert.assertTrue(yesterdayPeriod.compareTo(todayPeriod) < 0);
+ Assert.assertTrue(todayPeriod.compareTo(todayPeriod) == 0);
+ Assert.assertTrue(tommorrowPeriod.compareTo(todayPeriod) > 0);
+ }
+
+}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionServiceTest.java
index 85cfa30..1cc2fb6 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionServiceTest.java
@@ -363,7 +363,7 @@
@Test
public void getScheduledActions() throws Exception {
final ScheduledActionService testSubject = new ScheduledActionService();
- final List<ScheduledAction> result = testSubject.getScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
+ final List<ScheduledAction> result = testSubject.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
Assert.assertTrue(testCase.description, result.containsAll(testCase.expectedResultContents));
result.forEach(x -> {