* Split costumer loan account into: principal, interest and fees.
* Adjusted charges accordingly.
* Fixed overflowing account and ledger identifiers by shortening account
designators, and truncating customer name.
* Added some unit tests.
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java
index 912ef35..3fd48ff 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java
@@ -20,15 +20,21 @@
*/
@SuppressWarnings("unused")
public interface AccountDesignators {
- String CUSTOMER_LOAN = "customer-loan";
- String LOAN_FUNDS_SOURCE = "loan-funds-source";
- String PROCESSING_FEE_INCOME = "processing-fee-income";
- String ORIGINATION_FEE_INCOME = "origination-fee-income";
- String DISBURSEMENT_FEE_INCOME = "disbursement-fee-income";
- String INTEREST_INCOME = "interest-income";
- String INTEREST_ACCRUAL = "interest-accrual";
- String LATE_FEE_INCOME = "late-fee-income";
- String LATE_FEE_ACCRUAL = "late-fee-accrual";
- String ARREARS_ALLOWANCE = "arrears-allowance";
- String ENTRY = "entry";
+ //These are maximum 3 characters because they are used to create account and ledger identifiers.
+ //Account and ledger identifiers are limited to 34 characters, and 32 characters respectively.
+ //These accounting identifiers are composed of the customer identifier, this identifier, and a counter.
+ String CUSTOMER_LOAN_GROUP = "cll";
+ String CUSTOMER_LOAN_PRINCIPAL = "clp";
+ String CUSTOMER_LOAN_INTEREST = "cli";
+ String CUSTOMER_LOAN_FEES = "clf";
+ String LOAN_FUNDS_SOURCE = "ls";
+ String PROCESSING_FEE_INCOME = "pfi";
+ String ORIGINATION_FEE_INCOME = "ofi";
+ String DISBURSEMENT_FEE_INCOME = "dfi";
+ String INTEREST_INCOME = "ii";
+ String INTEREST_ACCRUAL = "ia";
+ String LATE_FEE_INCOME = "lfi";
+ String LATE_FEE_ACCRUAL = "lfa";
+ String ARREARS_ALLOWANCE = "aa";
+ String ENTRY = "ey";
}
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
index f9fbbd9..2f570ac 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
@@ -36,8 +36,12 @@
String LOAN_ORIGINATION_FEE_ID = "loan-origination-fee";
String PROCESSING_FEE_NAME = "Processing fee";
String PROCESSING_FEE_ID = "processing-fee";
- String REPAYMENT_NAME = "Repayment";
- String REPAYMENT_ID = "repayment";
+ String REPAY_PRINCIPAL_NAME = "Repay principal";
+ String REPAY_PRINCIPAL_ID = "repay-principal";
+ String REPAY_INTEREST_NAME = "Repay interest";
+ String REPAY_INTEREST_ID = "repay-interest";
+ String REPAY_FEES_NAME = "Repay fees";
+ String REPAY_FEES_ID = "repay-fees";
static String nameToIdentifier(String name) {
return name.toLowerCase(Locale.US).replace(" ", "-");
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Pattern.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Pattern.java
index 66e7314..55381ce 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Pattern.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Pattern.java
@@ -15,6 +15,7 @@
*/
package io.mifos.portfolio.api.v1.domain;
+import java.util.Objects;
import java.util.Set;
/**
@@ -23,16 +24,12 @@
@SuppressWarnings({"WeakerAccess", "unused"})
public class Pattern {
private String parameterPackage;
- private Set<String> accountAssignmentsRequired;
+ private Set<String> accountAssignmentGroups;
+ private Set<RequiredAccountAssignment> accountAssignmentsRequired;
public Pattern() {
}
- public Pattern(String parametersNameSpace, Set<String> accountAssignmentsRequired) {
- this.parameterPackage = parametersNameSpace;
- this.accountAssignmentsRequired = accountAssignmentsRequired;
- }
-
public String getParameterPackage() {
return parameterPackage;
}
@@ -41,11 +38,19 @@
this.parameterPackage = parameterPackage;
}
- public Set<String> getAccountAssignmentsRequired() {
+ public Set<String> getAccountAssignmentGroups() {
+ return accountAssignmentGroups;
+ }
+
+ public void setAccountAssignmentGroups(Set<String> accountAssignmentGroups) {
+ this.accountAssignmentGroups = accountAssignmentGroups;
+ }
+
+ public Set<RequiredAccountAssignment> getAccountAssignmentsRequired() {
return accountAssignmentsRequired;
}
- public void setAccountAssignmentsRequired(Set<String> accountAssignmentsRequired) {
+ public void setAccountAssignmentsRequired(Set<RequiredAccountAssignment> accountAssignmentsRequired) {
this.accountAssignmentsRequired = accountAssignmentsRequired;
}
@@ -53,25 +58,23 @@
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
-
Pattern pattern = (Pattern) o;
-
- return parameterPackage != null ? parameterPackage.equals(pattern.parameterPackage) : pattern.parameterPackage == null && (accountAssignmentsRequired != null ? accountAssignmentsRequired.equals(pattern.accountAssignmentsRequired) : pattern.accountAssignmentsRequired == null);
-
+ return Objects.equals(parameterPackage, pattern.parameterPackage) &&
+ Objects.equals(accountAssignmentGroups, pattern.accountAssignmentGroups) &&
+ Objects.equals(accountAssignmentsRequired, pattern.accountAssignmentsRequired);
}
@Override
public int hashCode() {
- int result = parameterPackage != null ? parameterPackage.hashCode() : 0;
- result = 31 * result + (accountAssignmentsRequired != null ? accountAssignmentsRequired.hashCode() : 0);
- return result;
+ return Objects.hash(parameterPackage, accountAssignmentGroups, accountAssignmentsRequired);
}
@Override
public String toString() {
return "Pattern{" +
- "parameterPackage='" + parameterPackage + '\'' +
- ", accountAssignmentsRequired=" + accountAssignmentsRequired +
- '}';
+ "parameterPackage='" + parameterPackage + '\'' +
+ ", accountAssignmentGroups=" + accountAssignmentGroups +
+ ", accountAssignmentsRequired=" + accountAssignmentsRequired +
+ '}';
}
}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/RequiredAccountAssignment.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/RequiredAccountAssignment.java
new file mode 100644
index 0000000..1cea505
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/RequiredAccountAssignment.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.portfolio.api.v1.domain;
+
+import javax.annotation.Nullable;
+import java.util.Objects;
+
+@SuppressWarnings("unused")
+public class RequiredAccountAssignment {
+ private String accountDesignator;
+ private String accountType;
+ private @Nullable String group;
+
+ public RequiredAccountAssignment(String accountDesignator, String accountType) {
+ this.accountDesignator = accountDesignator;
+ this.accountType = accountType;
+ this.group = null;
+ }
+
+ public RequiredAccountAssignment(String accountDesignator, String accountType, String ledger) {
+ this.accountDesignator = accountDesignator;
+ this.accountType = accountType;
+ this.group = ledger;
+ }
+
+ public String getAccountDesignator() {
+ return accountDesignator;
+ }
+
+ public void setAccountDesignator(String accountDesignator) {
+ this.accountDesignator = accountDesignator;
+ }
+
+ public String getAccountType() {
+ return accountType;
+ }
+
+ public void setAccountType(String thothType) {
+ this.accountType = thothType;
+ }
+
+ public String getGroup() {
+ return group;
+ }
+
+ public void setGroup(String group) {
+ this.group = group;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RequiredAccountAssignment that = (RequiredAccountAssignment) o;
+ return Objects.equals(accountDesignator, that.accountDesignator) &&
+ Objects.equals(accountType, that.accountType) &&
+ Objects.equals(group, that.group);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(accountDesignator, accountType, group);
+ }
+
+ @Override
+ public String toString() {
+ return "RequiredAccountAssignment{" +
+ "accountDesignator='" + accountDesignator + '\'' +
+ ", accountType='" + accountType + '\'' +
+ ", group='" + group + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/test/java/io/mifos/Fixture.java b/api/src/test/java/io/mifos/Fixture.java
index a6f1435..55fe883 100644
--- a/api/src/test/java/io/mifos/Fixture.java
+++ b/api/src/test/java/io/mifos/Fixture.java
@@ -67,7 +67,7 @@
//accountAssignments.add(new AccountAssignment(ENTRY, ...));
// Don't assign entry account in test since it usually will not be assigned IRL.
accountAssignments.add(new AccountAssignment(LOAN_FUNDS_SOURCE, LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER));
- accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN, "001-013"));
+ accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN_GROUP, "001-013"));
product.setAccountAssignments(accountAssignments);
final ProductParameters productParameters = new ProductParameters();
@@ -104,7 +104,7 @@
final Set<AccountAssignment> accountAssignments = new HashSet<>();
- accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN, "001-011"));
+ accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN_GROUP, "001-011"));
accountAssignments.add(new AccountAssignment(ENTRY, "001-012"));
ret.setAccountAssignments(accountAssignments);
ret.setCurrentState(Case.State.CREATED.name());
diff --git a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
index 7df57bf..feaee49 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -21,6 +21,7 @@
import io.mifos.core.lang.DateConverter;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import org.hamcrest.Description;
+import org.junit.Assert;
import org.mockito.AdditionalMatchers;
import org.mockito.ArgumentMatcher;
import org.mockito.Matchers;
@@ -100,6 +101,15 @@
.fetchAccountEntriesStream(Mockito.eq(account.getIdentifier()), Matchers.anyString(), Matchers.anyString(), AdditionalMatchers.or(Matchers.eq("DESC"), Matchers.eq("ASC")));
}
+ private static void makeLedgerResponsive(
+ final Ledger ledger,
+ final LedgerManager ledgerManagerMock)
+ {
+ Mockito.doReturn(ledger).when(ledgerManagerMock).findLedger(ledger.getIdentifier());
+ Mockito.doReturn(emptyAccountsPage()).when(ledgerManagerMock).fetchAccountsOfLedger(Mockito.eq(ledger.getIdentifier()),
+ Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
+ }
+
private static Ledger cashLedger() {
final Ledger ret = new Ledger();
@@ -231,7 +241,7 @@
private static Account arrearsAllowanceAccount() {
final Account ret = new Account();
ret.setIdentifier(ARREARS_ALLOWANCE_ACCOUNT_IDENTIFIER);
- //ret.setLedger(LOAN_INCOME_LEDGER_IDENTIFIER); //TODO: ??
+ //ret.setGroup(LOAN_INCOME_LEDGER_IDENTIFIER); //TODO: ??
ret.setType(AccountType.LIABILITY.name()); //TODO: ??
return ret;
}
@@ -251,6 +261,14 @@
return ret;
}
+ private static AccountPage emptyAccountsPage() {
+ final AccountPage ret = new AccountPage();
+ ret.setTotalElements(0L);
+ ret.setTotalPages(1);
+ ret.setAccounts(Collections.emptyList());
+ return ret;
+ }
+
private static <T> Valid<T> isValid() {
return new Valid<>();
}
@@ -269,11 +287,16 @@
private static class AccountMatcher extends ArgumentMatcher<Account> {
private final String ledgerIdentifer;
+ private final String accountDesignator;
private final AccountType type;
private Account matchedArgument;
- private AccountMatcher(final String ledgerIdentifier, final AccountType type) {
+ private AccountMatcher(
+ final String ledgerIdentifier,
+ final String accountDesignator,
+ final AccountType type) {
this.ledgerIdentifer = ledgerIdentifier;
+ this.accountDesignator = accountDesignator;
this.type = type;
this.matchedArgument = null; //Set when matches called and returns true.
}
@@ -288,6 +311,7 @@
final Account checkedArgument = (Account) argument;
final boolean ret = checkedArgument.getLedger().equals(ledgerIdentifer) &&
+ checkedArgument.getIdentifier().contains(accountDesignator) &&
checkedArgument.getType().equals(type.name()) &&
checkedArgument.getBalance() == 0.0;
@@ -297,9 +321,75 @@
return ret;
}
+ @Override
+ public void describeTo(final Description description) {
+ description.appendText(this.toString());
+ }
+
Account getMatchedArgument() {
return matchedArgument;
}
+
+ @Override
+ public String toString() {
+ return "AccountMatcher{" +
+ "ledgerIdentifer='" + ledgerIdentifer + '\'' +
+ ", accountDesignator='" + accountDesignator + '\'' +
+ ", type=" + type +
+ '}';
+ }
+ }
+
+ private static class LedgerMatcher extends ArgumentMatcher<Ledger> {
+ private final String ledgerIdentifer;
+ private final AccountType type;
+ private Ledger matchedArgument;
+
+ LedgerMatcher(String ledgerIdentifier, AccountType type) {
+ this.ledgerIdentifer = ledgerIdentifier;
+ this.type = type;
+ this.matchedArgument = null; //Set when matches called and returns true.
+ }
+
+ @Override
+ public boolean matches(final Object argument) {
+ if (argument == null)
+ return false;
+ if (! (argument instanceof Ledger))
+ return false;
+
+ final Ledger checkedArgument = (Ledger) argument;
+
+ final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+ final Set errors = validator.validate(checkedArgument);
+
+ Assert.assertEquals(0, errors.size());
+
+ final boolean ret = checkedArgument.getParentLedgerIdentifier().equals(ledgerIdentifer) &&
+ checkedArgument.getType().equals(type.name());
+
+ if (ret)
+ matchedArgument = checkedArgument;
+
+ return ret;
+ }
+
+ @Override
+ public void describeTo(final Description description) {
+ description.appendText(this.toString());
+ }
+
+ Ledger getMatchedArgument() {
+ return matchedArgument;
+ }
+
+ @Override
+ public String toString() {
+ return "LedgerMatcher{" +
+ "ledgerIdentifer='" + ledgerIdentifer + '\'' +
+ ", type=" + type +
+ '}';
+ }
}
private static class JournalEntryMatcher extends ArgumentMatcher<JournalEntry> {
@@ -386,6 +476,15 @@
}
}
+ private static class CreateLedgerAnswer implements Answer {
+ @Override
+ public Void answer(final InvocationOnMock invocation) throws Throwable {
+ final Ledger ledger = invocation.getArgumentAt(0, Ledger.class);
+ makeLedgerResponsive(ledger, (LedgerManager) invocation.getMock());
+ return null;
+ }
+ }
+
static class AccountEntriesStreamAnswer implements Answer {
private final AccountData accountData;
@@ -437,20 +536,33 @@
Mockito.doAnswer(new FindAccountAnswer()).when(ledgerManagerMock).findAccount(Matchers.anyString());
Mockito.doAnswer(new CreateAccountAnswer()).when(ledgerManagerMock).createAccount(Matchers.any());
Mockito.doAnswer(new CreateJournalEntryAnswer()).when(ledgerManagerMock).createJournalEntry(Matchers.any(JournalEntry.class));
+ Mockito.doAnswer(new CreateLedgerAnswer()).when(ledgerManagerMock).createLedger(Matchers.any(Ledger.class));
}
static void mockBalance(final String accountIdentifier, final BigDecimal balance) {
accountMap.get(accountIdentifier).setBalance(balance.doubleValue());
}
- static String verifyAccountCreation(final LedgerManager ledgerManager,
- final String ledgerIdentifier,
- final AccountType type) {
- final AccountMatcher specifiesCorrectAccount = new AccountMatcher(ledgerIdentifier, type);
+ static String verifyAccountCreationMatchingDesignator(
+ final LedgerManager ledgerManager,
+ final String ledgerIdentifier,
+ final String accountDesignator,
+ final AccountType type) {
+ final AccountMatcher specifiesCorrectAccount = new AccountMatcher(ledgerIdentifier, accountDesignator, type);
Mockito.verify(ledgerManager).createAccount(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectAccount)));
return specifiesCorrectAccount.getMatchedArgument().getIdentifier();
}
+ static String verifyLedgerCreation(
+ final LedgerManager ledgerManager,
+ final String ledgerIdentifier,
+ final AccountType type) {
+ final LedgerMatcher specifiesCorrectLedger = new LedgerMatcher(ledgerIdentifier, type);
+ Mockito.verify(ledgerManager).createLedger(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectLedger)));
+ makeLedgerResponsive(specifiesCorrectLedger.getMatchedArgument(), ledgerManager);
+ return specifiesCorrectLedger.getMatchedArgument().getIdentifier();
+ }
+
static void verifyTransfer(final LedgerManager ledgerManager,
final Set<Debtor> debtors,
final Set<Creditor> creditors,
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 900ac99..2623acf 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -19,6 +19,7 @@
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.caseinstance.CreditWorthinessFactor;
import io.mifos.individuallending.api.v1.domain.caseinstance.CreditWorthinessSnapshot;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.product.ProductParameters;
import io.mifos.portfolio.api.v1.domain.*;
@@ -71,10 +72,20 @@
//accountAssignments.add(new AccountAssignment(ENTRY, ...));
// Don't assign entry account in test since it usually will not be assigned IRL.
accountAssignments.add(new AccountAssignment(LOAN_FUNDS_SOURCE, LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER));
- final AccountAssignment customerLoanAccountAssignment = new AccountAssignment();
- customerLoanAccountAssignment.setDesignator(CUSTOMER_LOAN);
- customerLoanAccountAssignment.setLedgerIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
- accountAssignments.add(customerLoanAccountAssignment);
+ final AccountAssignment customerLoanPrincipalAccountAssignment = new AccountAssignment();
+ customerLoanPrincipalAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ customerLoanPrincipalAccountAssignment.setLedgerIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
+ accountAssignments.add(customerLoanPrincipalAccountAssignment);
+
+ final AccountAssignment customerLoanInterestAccountAssignment = new AccountAssignment();
+ customerLoanInterestAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_INTEREST);
+ customerLoanInterestAccountAssignment.setLedgerIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
+ accountAssignments.add(customerLoanInterestAccountAssignment);
+
+ final AccountAssignment customerLoanFeesAccountAssignment = new AccountAssignment();
+ customerLoanFeesAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_FEES);
+ customerLoanFeesAccountAssignment.setLedgerIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
+ accountAssignments.add(customerLoanFeesAccountAssignment);
product.setAccountAssignments(accountAssignments);
final ProductParameters productParameters = new ProductParameters();
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
index def64ad..46421e9 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -68,7 +68,9 @@
private Product product = null;
private Case customerCase = null;
private TaskDefinition taskDefinition = null;
- private String customerLoanAccountIdentifier = null;
+ private String customerLoanPrincipalIdentifier = null;
+ private String customerLoanInterestIdentifier = null;
+ private String customerLoanFeeIdentifier = null;
private BigDecimal expectedCurrentBalance = null;
private BigDecimal interestAccrued = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
@@ -212,7 +214,7 @@
while (expectedCurrentBalance.compareTo(BigDecimal.ZERO) > 0) {
logger.info("Simulating week {}. Expected current balance {}.", week, expectedCurrentBalance);
if (week == weekOfLateRepayment) {
- final BigDecimal lateFee = BigDecimal.valueOf(17_31, MINOR_CURRENCY_UNIT_DIGITS);
+ final BigDecimal lateFee = BigDecimal.valueOf(31_18, MINOR_CURRENCY_UNIT_DIGITS);
step6CalculateInterestAndCheckForLatenessForRangeOfDays(
today,
(week * 7) + 1,
@@ -242,7 +244,7 @@
private BigDecimal findNextRepaymentAmount(
final LocalDateTime referenceDate,
final int dayNumber) {
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
+ AccountingFixture.mockBalance(customerLoanPrincipalIdentifier, expectedCurrentBalance);
final Payment nextPayment = portfolioManager.getCostComponentsForAction(
product.getIdentifier(),
@@ -251,7 +253,7 @@
null,
null,
DateConverter.toIsoString(referenceDate.plusDays(dayNumber)));
- return nextPayment.getCostComponents().stream().filter(x -> x.getChargeIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID)).findFirst()
+ return nextPayment.getCostComponents().stream().filter(x -> x.getChargeIdentifier().equals(ChargeIdentifiers.REPAY_PRINCIPAL_ID)).findFirst()
.orElseThrow(() -> new IllegalArgumentException("return missing repayment charge."))
.getAmount();
}
@@ -382,8 +384,17 @@
Case.State.APPROVED);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Action.CLOSE);
- customerLoanAccountIdentifier =
- AccountingFixture.verifyAccountCreation(ledgerManager, AccountingFixture.CUSTOMER_LOAN_LEDGER_IDENTIFIER, AccountType.ASSET);
+ final String customerLoanLedgerIdentifier = AccountingFixture.verifyLedgerCreation(
+ ledgerManager,
+ AccountingFixture.CUSTOMER_LOAN_LEDGER_IDENTIFIER,
+ AccountType.ASSET);
+
+ customerLoanPrincipalIdentifier =
+ AccountingFixture.verifyAccountCreationMatchingDesignator(ledgerManager, customerLoanLedgerIdentifier, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, AccountType.ASSET);
+ customerLoanInterestIdentifier =
+ AccountingFixture.verifyAccountCreationMatchingDesignator(ledgerManager, customerLoanLedgerIdentifier, AccountDesignators.CUSTOMER_LOAN_INTEREST, AccountType.ASSET);
+ customerLoanFeeIdentifier =
+ AccountingFixture.verifyAccountCreationMatchingDesignator(ledgerManager, customerLoanLedgerIdentifier, AccountDesignators.CUSTOMER_LOAN_FEES, AccountType.ASSET);
expectedCurrentBalance = BigDecimal.ZERO;
}
@@ -398,7 +409,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE,
- Sets.newLinkedHashSet(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN),
+ Sets.newLinkedHashSet(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN_GROUP),
amount, new CostComponent(whichDisbursementFee, disbursementFeeAmount),
new CostComponent(ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, LOAN_ORIGINATION_FEE_AMOUNT),
new CostComponent(ChargeIdentifiers.PROCESSING_FEE_ID, PROCESSING_FEE_AMOUNT),
@@ -417,10 +428,10 @@
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
final Set<Debtor> debtors = new HashSet<>();
- debtors.add(new Debtor(customerLoanAccountIdentifier, amount.toPlainString()));
- debtors.add(new Debtor(customerLoanAccountIdentifier, PROCESSING_FEE_AMOUNT.toPlainString()));
- debtors.add(new Debtor(customerLoanAccountIdentifier, disbursementFeeAmount.toPlainString()));
- debtors.add(new Debtor(customerLoanAccountIdentifier, LOAN_ORIGINATION_FEE_AMOUNT.toPlainString()));
+ debtors.add(new Debtor(customerLoanPrincipalIdentifier, amount.toPlainString()));
+ debtors.add(new Debtor(customerLoanFeeIdentifier, PROCESSING_FEE_AMOUNT.toPlainString()));
+ debtors.add(new Debtor(customerLoanFeeIdentifier, disbursementFeeAmount.toPlainString()));
+ debtors.add(new Debtor(customerLoanFeeIdentifier, LOAN_ORIGINATION_FEE_AMOUNT.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, amount.toString()));
@@ -482,7 +493,7 @@
final String beatIdentifier = "alignment0";
final String midnightTimeStamp = DateConverter.toIsoString(forTime);
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
+ AccountingFixture.mockBalance(customerLoanPrincipalIdentifier, expectedCurrentBalance);
final BigDecimal calculatedInterest = expectedCurrentBalance.multiply(Fixture.INTEREST_RATE.divide(Fixture.ACCRUAL_PERIODS, 8, BigDecimal.ROUND_HALF_EVEN))
.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
@@ -525,7 +536,7 @@
final Set<Debtor> debtors = new HashSet<>();
debtors.add(new Debtor(
- customerLoanAccountIdentifier,
+ customerLoanInterestIdentifier,
calculatedInterest.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
@@ -544,15 +555,15 @@
final BigDecimal lateFee) throws InterruptedException {
logger.info("step7PaybackPartialAmount '{}'", amount);
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
+ AccountingFixture.mockBalance(customerLoanPrincipalIdentifier, expectedCurrentBalance);
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.ACCEPT_PAYMENT,
- new HashSet<>(Arrays.asList(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN, AccountDesignators.LOAN_FUNDS_SOURCE)),
+ new HashSet<>(Arrays.asList(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN_GROUP, AccountDesignators.LOAN_FUNDS_SOURCE)),
amount,
- new CostComponent(ChargeIdentifiers.REPAYMENT_ID, amount),
+ new CostComponent(ChargeIdentifiers.REPAY_PRINCIPAL_ID, amount),
new CostComponent(ChargeIdentifiers.INTEREST_ID, interestAccrued),
new CostComponent(ChargeIdentifiers.LATE_FEE_ID, lateFee));
checkStateTransfer(
@@ -576,7 +587,7 @@
debtors.add(new Debtor(AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER, lateFee.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
- creditors.add(new Creditor(customerLoanAccountIdentifier, amount.toPlainString()));
+ creditors.add(new Creditor(customerLoanPrincipalIdentifier, amount.toPlainString()));
if (interestAccrued.compareTo(BigDecimal.ZERO) != 0)
creditors.add(new Creditor(AccountingFixture.CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
if (lateFee.compareTo(BigDecimal.ZERO) != 0)
@@ -591,7 +602,7 @@
private void step8Close() throws InterruptedException {
logger.info("step8Close");
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
+ AccountingFixture.mockBalance(customerLoanPrincipalIdentifier, expectedCurrentBalance);
checkCostComponentForActionCorrect(
product.getIdentifier(),
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestCases.java b/component-test/src/main/java/io/mifos/portfolio/TestCases.java
index b6afb42..0206d75 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCases.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCases.java
@@ -21,6 +21,7 @@
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.caseinstance.CreditWorthinessFactor;
import io.mifos.individuallending.api.v1.domain.caseinstance.CreditWorthinessSnapshot;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.CasePage;
@@ -37,9 +38,6 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.CUSTOMER_LOAN;
-import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.ENTRY;
-
/**
* @author Myrle Krantz
*/
@@ -96,8 +94,8 @@
final Case caseInstance = createAdjustedCase(product.getIdentifier(), x -> x.setParameters(originalParameters));
final Set<AccountAssignment> accountAssignments = new HashSet<>();
- accountAssignments.add(new AccountAssignment(CUSTOMER_LOAN, "002-011"));
- accountAssignments.add(new AccountAssignment(ENTRY, "002-012"));
+ accountAssignments.add(new AccountAssignment(AccountDesignators.CUSTOMER_LOAN_GROUP, "002-011"));
+ accountAssignments.add(new AccountAssignment(AccountDesignators.ENTRY, "002-012"));
caseInstance.setAccountAssignments(accountAssignments);
newCaseParameters.setMaximumBalance(Fixture.fixScale(BigDecimal.TEN));
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
index ec4540d..a358757 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
@@ -59,7 +59,9 @@
ChargeIdentifiers.ALLOW_FOR_WRITE_OFF_ID,
ChargeIdentifiers.DISBURSE_PAYMENT_ID,
ChargeIdentifiers.INTEREST_ID,
- ChargeIdentifiers.REPAYMENT_ID)
+ ChargeIdentifiers.REPAY_PRINCIPAL_ID,
+ ChargeIdentifiers.REPAY_INTEREST_ID,
+ ChargeIdentifiers.REPAY_FEES_ID)
.collect(Collectors.toSet());
final Set<String> expectedChangeableChargeDefinitionIdentifiers = Stream.of(
ChargeIdentifiers.DISBURSEMENT_FEE_ID,
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java b/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java
index 1f8c03e..4294623 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java
@@ -90,7 +90,7 @@
Assert.assertNotNull(paymentScheduleFirstPage);
paymentScheduleFirstPage.getElements().forEach(x -> {
x.getPayment().getCostComponents().forEach(y -> Assert.assertEquals(product.getMinorCurrencyUnitDigits(), y.getAmount().scale()));
- Assert.assertEquals(product.getMinorCurrencyUnitDigits(), x.getBalances().get(AccountDesignators.CUSTOMER_LOAN).scale());
+ Assert.assertEquals(product.getMinorCurrencyUnitDigits(), x.getBalances().get(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL).scale());
});
}
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index bd76bb4..c18c710 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -16,9 +16,11 @@
package io.mifos.individuallending;
import com.google.gson.Gson;
+import io.mifos.accounting.api.v1.domain.AccountType;
import io.mifos.core.lang.ServiceException;
import io.mifos.customer.api.v1.client.CustomerManager;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
@@ -45,7 +47,6 @@
import java.util.*;
import java.util.stream.Collectors;
-import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.*;
import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
/**
@@ -55,6 +56,62 @@
@Component
public class IndividualLendingPatternFactory implements PatternFactory {
final static private String INDIVIDUAL_LENDING_PACKAGE = "io.mifos.individuallending.api.v1";
+ final static private Pattern INDIVIDUAL_LENDING_PATTERN;
+
+ static {
+ INDIVIDUAL_LENDING_PATTERN = new Pattern();
+ INDIVIDUAL_LENDING_PATTERN.setParameterPackage(INDIVIDUAL_LENDING_PACKAGE);
+ INDIVIDUAL_LENDING_PATTERN.setAccountAssignmentGroups(Collections.singleton(AccountDesignators.CUSTOMER_LOAN_GROUP));
+ final Set<RequiredAccountAssignment> individualLendingRequiredAccounts = new HashSet<>();
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.CUSTOMER_LOAN_PRINCIPAL,
+ AccountType.ASSET.name(),
+ AccountDesignators.CUSTOMER_LOAN_GROUP));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.CUSTOMER_LOAN_INTEREST,
+ AccountType.ASSET.name(),
+ AccountDesignators.CUSTOMER_LOAN_GROUP));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountType.ASSET.name(),
+ AccountDesignators.CUSTOMER_LOAN_GROUP));
+
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.LOAN_FUNDS_SOURCE,
+ AccountType.ASSET.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.PROCESSING_FEE_INCOME,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.ORIGINATION_FEE_INCOME,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.DISBURSEMENT_FEE_INCOME,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.INTEREST_INCOME,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.INTEREST_ACCRUAL,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.LATE_FEE_INCOME,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.LATE_FEE_ACCRUAL,
+ AccountType.REVENUE.name()));
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.ARREARS_ALLOWANCE,
+ AccountType.LIABILITY.name())); //TODO: type?
+ individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
+ AccountDesignators.ENTRY,
+ AccountType.LIABILITY.name()));
+ INDIVIDUAL_LENDING_PATTERN.setAccountAssignmentsRequired(individualLendingRequiredAccounts);
+ }
+ public static Pattern individualLendingPattern() {
+ return INDIVIDUAL_LENDING_PATTERN;
+ }
+
private final CaseParametersRepository caseParametersRepository;
private final DataContextService dataContextService;
private final CostComponentService costComponentService;
@@ -81,110 +138,38 @@
@Override
public Pattern pattern() {
-
- final Set<String> individualLendingRequiredAccounts = new HashSet<>();
- individualLendingRequiredAccounts.add(CUSTOMER_LOAN);
- //TODO: fix in migration individualLendingRequiredAccounts.add(PENDING_DISBURSAL);
- //was String PENDING_DISBURSAL = "pending-disbursal";
- individualLendingRequiredAccounts.add(LOAN_FUNDS_SOURCE);
- individualLendingRequiredAccounts.add(PROCESSING_FEE_INCOME);
- individualLendingRequiredAccounts.add(ORIGINATION_FEE_INCOME);
- individualLendingRequiredAccounts.add(DISBURSEMENT_FEE_INCOME);
- individualLendingRequiredAccounts.add(INTEREST_INCOME);
- individualLendingRequiredAccounts.add(INTEREST_ACCRUAL);
- individualLendingRequiredAccounts.add(LATE_FEE_INCOME);
- individualLendingRequiredAccounts.add(LATE_FEE_ACCRUAL);
- individualLendingRequiredAccounts.add(ARREARS_ALLOWANCE);
- individualLendingRequiredAccounts.add(ENTRY);
- return new Pattern(INDIVIDUAL_LENDING_PACKAGE, individualLendingRequiredAccounts);
+ return INDIVIDUAL_LENDING_PATTERN;
}
@Override
public List<ChargeDefinition> charges() {
- return defaultIndividualLoanCharges();
+ final List<ChargeDefinition> ret = defaultIndividualLoanCharges();
+ ret.addAll(requiredIndividualLoanCharges());
+ return ret;
}
- public static List<ChargeDefinition> defaultIndividualLoanCharges() {
+ public static List<ChargeDefinition> requiredIndividualLoanCharges() {
final List<ChargeDefinition> ret = new ArrayList<>();
- final ChargeDefinition processingFee = charge(
- PROCESSING_FEE_NAME,
- Action.DISBURSE, //TODO: fix existing charges in migration
- BigDecimal.ONE,
- CUSTOMER_LOAN, //TODO: fix existing charges in migration
- PROCESSING_FEE_INCOME);
- processingFee.setReadOnly(false);
-
- final ChargeDefinition loanOriginationFee = charge(
- LOAN_ORIGINATION_FEE_NAME,
- Action.DISBURSE, //TODO: fix existing charges in migration
- BigDecimal.ONE,
- CUSTOMER_LOAN, //TODO: fix existing charges in migration
- ORIGINATION_FEE_INCOME);
- loanOriginationFee.setReadOnly(false);
-
- /*final ChargeDefinition loanFundsAllocation = charge(
- LOAN_FUNDS_ALLOCATION_ID,
- Action.APPROVE,
- BigDecimal.valueOf(100),
- LOAN_FUNDS_SOURCE,
- PENDING_DISBURSAL);
- loanFundsAllocation.setReadOnly(true);*/
- //TODO: handle removing this extraneous charge in migration.
-
- final ChargeDefinition disbursementFee = charge(
- DISBURSEMENT_FEE_NAME,
- Action.DISBURSE,
- BigDecimal.valueOf(0.1),
- CUSTOMER_LOAN, //TODO: fix existing charges in migration
- DISBURSEMENT_FEE_INCOME);
- disbursementFee.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue()); //TODO: fix existing charges in migration
- disbursementFee.setReadOnly(false);
final ChargeDefinition disbursePayment = new ChargeDefinition();
disbursePayment.setChargeAction(Action.DISBURSE.name());
disbursePayment.setIdentifier(DISBURSE_PAYMENT_ID);
disbursePayment.setName(DISBURSE_PAYMENT_NAME);
disbursePayment.setDescription(DISBURSE_PAYMENT_NAME);
- disbursePayment.setFromAccountDesignator(CUSTOMER_LOAN); //TODO: fix existing charges in migration
- disbursePayment.setToAccountDesignator(ENTRY);
+ disbursePayment.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ disbursePayment.setToAccountDesignator(AccountDesignators.ENTRY);
disbursePayment.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
disbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
disbursePayment.setAmount(BigDecimal.valueOf(100));
disbursePayment.setReadOnly(true);
- /*
- final ChargeDefinition trackPrincipalDisbursePayment = new ChargeDefinition();
- trackPrincipalDisbursePayment.setChargeAction(Action.DISBURSE.name());
- trackPrincipalDisbursePayment.setIdentifier(TRACK_DISBURSAL_PAYMENT_ID);
- trackPrincipalDisbursePayment.setName(TRACK_DISBURSAL_PAYMENT_NAME);
- trackPrincipalDisbursePayment.setDescription(TRACK_DISBURSAL_PAYMENT_NAME);
- trackPrincipalDisbursePayment.setFromAccountDesignator(PENDING_DISBURSAL);
- trackPrincipalDisbursePayment.setToAccountDesignator(CUSTOMER_LOAN);
- trackPrincipalDisbursePayment.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
- trackPrincipalDisbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
- trackPrincipalDisbursePayment.setAmount(BigDecimal.valueOf(100));
- trackPrincipalDisbursePayment.setReadOnly(true);*/
- //TODO: handle removing this extraneous charge in migration.
-
- final ChargeDefinition lateFee = charge(
- LATE_FEE_NAME,
- Action.ACCEPT_PAYMENT,
- BigDecimal.TEN,
- CUSTOMER_LOAN,
- LATE_FEE_INCOME);
- lateFee.setAccrueAction(Action.MARK_LATE.name());
- lateFee.setAccrualAccountDesignator(LATE_FEE_ACCRUAL);
- lateFee.setProportionalTo(ChargeProportionalDesignator.CONTRACTUAL_REPAYMENT_DESIGNATOR.getValue());
- lateFee.setChargeOnTop(true);
- lateFee.setReadOnly(false);
-
//TODO: Make multiple write off allowance charges.
final ChargeDefinition writeOffAllowanceCharge = charge(
ALLOW_FOR_WRITE_OFF_NAME,
Action.MARK_LATE,
BigDecimal.valueOf(30),
- LOAN_FUNDS_SOURCE, //TODO: this and previous value ("pending-disbursal") are not correct and will require migration.
- ARREARS_ALLOWANCE);
+ AccountDesignators.LOAN_FUNDS_SOURCE,
+ AccountDesignators.ARREARS_ALLOWANCE);
writeOffAllowanceCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
writeOffAllowanceCharge.setReadOnly(true);
@@ -192,62 +177,105 @@
INTEREST_NAME,
Action.ACCEPT_PAYMENT,
BigDecimal.valueOf(100),
- CUSTOMER_LOAN,
- INTEREST_INCOME);
+ AccountDesignators.CUSTOMER_LOAN_INTEREST,
+ AccountDesignators.INTEREST_INCOME);
interestCharge.setForCycleSizeUnit(ChronoUnit.YEARS);
interestCharge.setAccrueAction(Action.APPLY_INTEREST.name());
- interestCharge.setAccrualAccountDesignator(INTEREST_ACCRUAL);
+ interestCharge.setAccrualAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
interestCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
interestCharge.setChargeMethod(ChargeDefinition.ChargeMethod.INTEREST);
interestCharge.setReadOnly(true);
- final ChargeDefinition customerRepaymentCharge = new ChargeDefinition();
- customerRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
- customerRepaymentCharge.setIdentifier(REPAYMENT_ID);
- customerRepaymentCharge.setName(REPAYMENT_NAME);
- customerRepaymentCharge.setDescription(REPAYMENT_NAME);
- customerRepaymentCharge.setFromAccountDesignator(ENTRY); //TODO: fix existing charges in migration
- customerRepaymentCharge.setToAccountDesignator(CUSTOMER_LOAN); //TODO: fix existing charges in migration
- customerRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REQUESTED_REPAYMENT_DESIGNATOR.getValue());
- customerRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
- customerRepaymentCharge.setAmount(BigDecimal.valueOf(100));
- customerRepaymentCharge.setReadOnly(true);
+ final ChargeDefinition customerPrincipalRepaymentCharge = new ChargeDefinition();
+ customerPrincipalRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
+ customerPrincipalRepaymentCharge.setIdentifier(REPAY_PRINCIPAL_ID);
+ customerPrincipalRepaymentCharge.setName(REPAY_PRINCIPAL_NAME);
+ customerPrincipalRepaymentCharge.setDescription(REPAY_PRINCIPAL_NAME);
+ customerPrincipalRepaymentCharge.setFromAccountDesignator(AccountDesignators.ENTRY);
+ customerPrincipalRepaymentCharge.setToAccountDesignator(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ customerPrincipalRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REQUESTED_REPAYMENT_DESIGNATOR.getValue());
+ customerPrincipalRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ customerPrincipalRepaymentCharge.setAmount(BigDecimal.valueOf(100));
+ customerPrincipalRepaymentCharge.setReadOnly(true);
- /*final ChargeDefinition trackReturnPrincipalCharge = new ChargeDefinition();
- trackReturnPrincipalCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
- trackReturnPrincipalCharge.setIdentifier(TRACK_RETURN_PRINCIPAL_ID);
- trackReturnPrincipalCharge.setName(TRACK_RETURN_PRINCIPAL_NAME);
- trackReturnPrincipalCharge.setDescription(TRACK_RETURN_PRINCIPAL_NAME);
- trackReturnPrincipalCharge.setFromAccountDesignator(LOAN_FUNDS_SOURCE);
- trackReturnPrincipalCharge.setToAccountDesignator(LOANS_PAYABLE);
- trackReturnPrincipalCharge.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
- trackReturnPrincipalCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
- trackReturnPrincipalCharge.setAmount(BigDecimal.valueOf(100));
- trackReturnPrincipalCharge.setReadOnly(true);*/
- //TODO: handle removing this extraneous charge in migration.
+ final ChargeDefinition customerInterestRepaymentCharge = new ChargeDefinition();
+ customerInterestRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
+ customerInterestRepaymentCharge.setIdentifier(REPAY_INTEREST_ID);
+ customerInterestRepaymentCharge.setName(REPAY_INTEREST_NAME);
+ customerInterestRepaymentCharge.setDescription(REPAY_INTEREST_NAME);
+ customerInterestRepaymentCharge.setFromAccountDesignator(AccountDesignators.ENTRY);
+ customerInterestRepaymentCharge.setToAccountDesignator(AccountDesignators.CUSTOMER_LOAN_INTEREST);
+ customerInterestRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REQUESTED_REPAYMENT_DESIGNATOR.getValue()); //TODO: ???
+ customerInterestRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL); //TODO: ???
+ customerInterestRepaymentCharge.setAmount(BigDecimal.valueOf(100)); //TODO: ???
+ customerInterestRepaymentCharge.setReadOnly(true);
- /*final ChargeDefinition disbursementReturnCharge = charge(
- RETURN_DISBURSEMENT_NAME,
- Action.CLOSE,
- BigDecimal.valueOf(100),
- PENDING_DISBURSAL,
- LOAN_FUNDS_SOURCE);
- disbursementReturnCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
- disbursementReturnCharge.setReadOnly(true);*/
- //TODO: handle removing this extraneous charge in migration.
+ final ChargeDefinition customerFeeRepaymentCharge = new ChargeDefinition();
+ customerFeeRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
+ customerFeeRepaymentCharge.setIdentifier(REPAY_FEES_ID);
+ customerFeeRepaymentCharge.setName(REPAY_FEES_NAME);
+ customerFeeRepaymentCharge.setDescription(REPAY_FEES_NAME);
+ customerFeeRepaymentCharge.setFromAccountDesignator(AccountDesignators.ENTRY);
+ customerFeeRepaymentCharge.setToAccountDesignator(AccountDesignators.CUSTOMER_LOAN_INTEREST);
+ customerFeeRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REQUESTED_REPAYMENT_DESIGNATOR.getValue()); //TODO: ???
+ customerFeeRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL); //TODO: ???
+ customerFeeRepaymentCharge.setAmount(BigDecimal.valueOf(100)); //TODO: ???
+ customerFeeRepaymentCharge.setReadOnly(true);
+
+ ret.add(disbursePayment);
+ ret.add(writeOffAllowanceCharge);
+ ret.add(interestCharge);
+ ret.add(customerPrincipalRepaymentCharge);
+ ret.add(customerInterestRepaymentCharge);
+ ret.add(customerFeeRepaymentCharge);
+
+ return ret;
+
+ }
+
+ public static List<ChargeDefinition> defaultIndividualLoanCharges() {
+ final List<ChargeDefinition> ret = new ArrayList<>();
+ final ChargeDefinition processingFee = charge(
+ PROCESSING_FEE_NAME,
+ Action.DISBURSE,
+ BigDecimal.ONE,
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountDesignators.PROCESSING_FEE_INCOME);
+ processingFee.setReadOnly(false);
+
+ final ChargeDefinition loanOriginationFee = charge(
+ LOAN_ORIGINATION_FEE_NAME,
+ Action.DISBURSE,
+ BigDecimal.ONE,
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountDesignators.ORIGINATION_FEE_INCOME);
+ loanOriginationFee.setReadOnly(false);
+
+ final ChargeDefinition disbursementFee = charge(
+ DISBURSEMENT_FEE_NAME,
+ Action.DISBURSE,
+ BigDecimal.valueOf(0.1),
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountDesignators.DISBURSEMENT_FEE_INCOME);
+ disbursementFee.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
+ disbursementFee.setReadOnly(false);
+
+ final ChargeDefinition lateFee = charge(
+ LATE_FEE_NAME,
+ Action.ACCEPT_PAYMENT,
+ BigDecimal.TEN,
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountDesignators.LATE_FEE_INCOME);
+ lateFee.setAccrueAction(Action.MARK_LATE.name());
+ lateFee.setAccrualAccountDesignator(AccountDesignators.LATE_FEE_ACCRUAL);
+ lateFee.setProportionalTo(ChargeProportionalDesignator.CONTRACTUAL_REPAYMENT_DESIGNATOR.getValue());
+ lateFee.setChargeOnTop(true);
+ lateFee.setReadOnly(false);
ret.add(processingFee);
ret.add(loanOriginationFee);
- //TODO: ret.add(loanFundsAllocation);
ret.add(disbursementFee);
- ret.add(disbursePayment);
- //TODO: ret.add(trackPrincipalDisbursePayment);
ret.add(lateFee);
- ret.add(writeOffAllowanceCharge);
- ret.add(interestCharge);
- ret.add(customerRepaymentCharge);
- //TODO: ret.add(trackReturnPrincipalCharge);
- //TODO: ret.add(disbursementReturnCharge);
return ret;
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
index cb63a4f..91c427f 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
@@ -127,16 +127,16 @@
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
+ final String customerLoanPrincipalAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
final String lateFeeAccrualAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.LATE_FEE_ACCRUAL);
- final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+ final BigDecimal currentBalance = accountingAdapter.getCurrentAccountBalance(customerLoanPrincipalAccountIdentifier);
if (currentBalance.compareTo(BigDecimal.ZERO) == 0) //No late fees if the current balance is zilch.
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getForTime());
final LocalDateTime dateOfMostRecentDisbursement =
- accountingAdapter.getDateOfMostRecentEntryContainingMessage(customerLoanAccountIdentifier, dataContextOfAction.getMessageForCharge(Action.DISBURSE))
+ accountingAdapter.getDateOfMostRecentEntryContainingMessage(customerLoanPrincipalAccountIdentifier, dataContextOfAction.getMessageForCharge(Action.DISBURSE))
.orElseThrow(() ->
ServiceException.badRequest("No last disbursal date for ''{0}.{1}'' could be determined. " +
"Therefore it cannot be checked for lateness.", productIdentifier, caseIdentifier));
@@ -155,7 +155,7 @@
.multiply(BigDecimal.valueOf(repaymentPeriodsBetweenBeginningAndToday));
final BigDecimal paymentsSum = accountingAdapter.sumMatchingEntriesSinceDate(
- customerLoanAccountIdentifier,
+ customerLoanPrincipalAccountIdentifier,
dateOfMostRecentDisbursement.toLocalDate(),
dataContextOfAction.getMessageForCharge(Action.ACCEPT_PAYMENT));
@@ -165,7 +165,7 @@
dataContextOfAction.getMessageForCharge(Action.MARK_LATE));
if (paymentsSum.compareTo(expectedPaymentSum.add(lateFeesAccrued)) < 0) {
- final Optional<LocalDateTime> dateOfMostRecentLateFee = accountingAdapter.getDateOfMostRecentEntryContainingMessage(customerLoanAccountIdentifier, dataContextOfAction.getMessageForCharge(Action.MARK_LATE));
+ final Optional<LocalDateTime> dateOfMostRecentLateFee = accountingAdapter.getDateOfMostRecentEntryContainingMessage(customerLoanPrincipalAccountIdentifier, dataContextOfAction.getMessageForCharge(Action.MARK_LATE));
if (!dateOfMostRecentLateFee.isPresent() ||
mostRecentLateFeeIsBeforeMostRecentRepaymentPeriod(repaymentPeriods, dateOfMostRecentLateFee.get())) {
commandBus.dispatch(new MarkLateCommand(productIdentifier, caseIdentifier, command.getForTime()));
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
index fb51ef3..5caf2ea 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
@@ -169,15 +169,27 @@
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ //Create the needed account assignments for groups and persist them for the case.
+ designatorToAccountIdentifierMapper.getGroupsNeedingLedgers()
+ .map(groupNeedingLedger -> new AccountAssignment(groupNeedingLedger.getGroupName(),
+ accountingAdapter.createLedger(
+ dataContextOfAction.getCaseParametersEntity().getCustomerIdentifier(),
+ groupNeedingLedger.getGroupName(),
+ groupNeedingLedger.getParentLedger())))
+ .map(accountAssignment -> CaseMapper.map(accountAssignment, dataContextOfAction.getCustomerCaseEntity()))
+ .forEach(caseAccountAssignmentEntity -> dataContextOfAction.getCustomerCaseEntity().getAccountAssignments().add(caseAccountAssignmentEntity));
+
//Create the needed account assignments and persist them for the case.
designatorToAccountIdentifierMapper.getLedgersNeedingAccounts()
- .map(ledger ->
- new AccountAssignment(ledger.getDesignator(),
- accountingAdapter.createAccountForLedgerAssignment(dataContextOfAction.getCaseParametersEntity().getCustomerIdentifier(), ledger)))
- .map(accountAssignment -> CaseMapper.map(accountAssignment, dataContextOfAction.getCustomerCaseEntity()))
- .forEach(caseAccountAssignmentEntity ->
- dataContextOfAction.getCustomerCaseEntity().getAccountAssignments().add(caseAccountAssignmentEntity)
- );
+ .map(ledger ->
+ new AccountAssignment(ledger.getDesignator(),
+ accountingAdapter.createAccountForLedgerAssignment(
+ dataContextOfAction.getCaseParametersEntity().getCustomerIdentifier(),
+ ledger)))
+ .map(accountAssignment -> CaseMapper.map(accountAssignment, dataContextOfAction.getCustomerCaseEntity()))
+ .forEach(caseAccountAssignmentEntity ->
+ dataContextOfAction.getCustomerCaseEntity().getAccountAssignments().add(caseAccountAssignmentEntity)
+ );
caseRepository.save(dataContextOfAction.getCustomerCaseEntity());
final PaymentBuilder paymentBuilder =
@@ -239,8 +251,10 @@
customerCase.setCurrentState(Case.State.ACTIVE.name());
caseRepository.save(customerCase);
}
- final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
- final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
+ final String customerLoanPrinicipalAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ final String customerLoanInterestAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_INTEREST);
+ final String customerLoanFeesAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_FEES);
+ final BigDecimal currentBalance = accountingAdapter.getTotalOfCurrentAccountBalances(customerLoanPrinicipalAccountIdentifier, customerLoanInterestAccountIdentifier, customerLoanFeesAccountIdentifier);
final BigDecimal newLoanPaymentSize = costComponentService.getLoanPaymentSizeForSingleDisbursement(
currentBalance.add(disbursalAmount),
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
index cbca5ee..69b7c33 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
@@ -158,9 +158,9 @@
final @Nullable BigDecimal requestedDisbursalSize) {
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
+ final String customerLoanPrincipalAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
final RealRunningBalances runningBalances = new RealRunningBalances(accountingAdapter, designatorToAccountIdentifierMapper);
- final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN);
+ final BigDecimal currentBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
if (requestedDisbursalSize != null &&
dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum().compareTo(
@@ -168,7 +168,7 @@
throw ServiceException.conflict("Cannot disburse over the maximum balance.");
final Optional<LocalDateTime> optionalStartOfTerm = accountingAdapter.getDateOfOldestEntryContainingMessage(
- customerLoanAccountIdentifier,
+ customerLoanPrincipalAccountIdentifier,
dataContextOfAction.getMessageForCharge(Action.DISBURSE));
final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
@@ -218,10 +218,9 @@
{
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
final RunningBalances runningBalances = new RealRunningBalances(accountingAdapter, designatorToAccountIdentifierMapper);
- final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
+ final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, designatorToAccountIdentifierMapper);
final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
@@ -262,10 +261,9 @@
{
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
final RealRunningBalances runningBalances = new RealRunningBalances(accountingAdapter, designatorToAccountIdentifierMapper);
- final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
+ final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, designatorToAccountIdentifierMapper);
final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
@@ -299,10 +297,10 @@
}
else {
if (scheduledAction.actionPeriod != null && scheduledAction.actionPeriod.isLastPeriod()) {
- loanPaymentSize = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN);
+ loanPaymentSize = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP);
}
else {
- final BigDecimal paymentSizeBeforeOnTopCharges = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN)
+ final BigDecimal paymentSizeBeforeOnTopCharges = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP)
.min(dataContextOfAction.getCaseParametersEntity().getPaymentSize());
@SuppressWarnings("UnnecessaryLocalVariable")
@@ -332,14 +330,12 @@
public PaymentBuilder getCostComponentsForClose(final DataContextOfAction dataContextOfAction) {
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
final RealRunningBalances runningBalances = new RealRunningBalances(accountingAdapter, designatorToAccountIdentifierMapper);
- if (runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN).compareTo(BigDecimal.ZERO) != 0)
+ if (runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP).compareTo(BigDecimal.ZERO) != 0)
throw ServiceException.conflict("Cannot close loan until the balance is zero.");
-
- final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
+ final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, designatorToAccountIdentifierMapper);
final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
@@ -379,10 +375,9 @@
{
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
final RunningBalances runningBalances = new RealRunningBalances(accountingAdapter, designatorToAccountIdentifierMapper);
- final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
+ final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, designatorToAccountIdentifierMapper);
final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
@@ -516,8 +511,8 @@
case MAXIMUM_BALANCE_DESIGNATOR:
return maximumBalance;
case RUNNING_BALANCE_DESIGNATOR: {
- final BigDecimal customerLoanRunningBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN);
- return customerLoanRunningBalance.subtract(paymentBuilder.getBalanceAdjustment(AccountDesignators.CUSTOMER_LOAN));
+ final BigDecimal customerLoanRunningBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP);
+ return customerLoanRunningBalance.subtract(paymentBuilder.getBalanceAdjustment(AccountDesignators.CUSTOMER_LOAN_GROUP));
}
case CONTRACTUAL_REPAYMENT_DESIGNATOR:
return contractualRepayment;
@@ -602,7 +597,7 @@
minorCurrencyUnitDigits,
false
);
- final BigDecimal finalDisbursementSize = paymentBuilder.getBalanceAdjustment(AccountDesignators.CUSTOMER_LOAN).negate();
+ final BigDecimal finalDisbursementSize = paymentBuilder.getBalanceAdjustment(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL).negate();
final MonetaryAmount presentValue = AnnuityPayment.calculate(
Money.of(finalDisbursementSize, "XXX"),
@@ -644,9 +639,12 @@
}
private LocalDate getStartOfTermOrThrow(final DataContextOfAction dataContextOfAction,
- final String customerLoanAccountIdentifier) {
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
+
+ final String customerLoanPrincipalAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+
final Optional<LocalDateTime> firstDisbursalDateTime = accountingAdapter.getDateOfOldestEntryContainingMessage(
- customerLoanAccountIdentifier,
+ customerLoanPrincipalAccountIdentifier,
dataContextOfAction.getMessageForCharge(Action.DISBURSE));
return firstDisbursalDateTime.map(LocalDateTime::toLocalDate)
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapper.java b/service/src/main/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapper.java
index 851bedd..fbc0273 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapper.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapper.java
@@ -16,17 +16,19 @@
package io.mifos.individuallending.internal.service;
import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.IndividualLendingPatternFactory;
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
+import io.mifos.portfolio.api.v1.domain.RequiredAccountAssignment;
import io.mifos.portfolio.service.internal.mapper.CaseMapper;
import io.mifos.portfolio.service.internal.mapper.ProductMapper;
import io.mifos.portfolio.service.internal.repository.CaseAccountAssignmentEntity;
import io.mifos.portfolio.service.internal.repository.ProductAccountAssignmentEntity;
import javax.annotation.Nonnull;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@@ -38,9 +40,19 @@
private final @Nonnull List<AccountAssignment> oneTimeAccountAssignments;
public DesignatorToAccountIdentifierMapper(final @Nonnull DataContextOfAction dataContextOfAction) {
- this.productAccountAssignments = dataContextOfAction.getProductEntity().getAccountAssignments();
- this.caseAccountAssignments = dataContextOfAction.getCustomerCaseEntity().getAccountAssignments();
- this.oneTimeAccountAssignments = dataContextOfAction.getOneTimeAccountAssignments();
+ this(dataContextOfAction.getProductEntity().getAccountAssignments(),
+ dataContextOfAction.getCustomerCaseEntity().getAccountAssignments(),
+ dataContextOfAction.getOneTimeAccountAssignments());
+ }
+
+ DesignatorToAccountIdentifierMapper(
+ final @Nonnull Set<ProductAccountAssignmentEntity> productAccountAssignments,
+ final @Nonnull Set<CaseAccountAssignmentEntity> caseAccountAssignments,
+ final @Nonnull List<AccountAssignment> oneTimeAccountAssignments) {
+
+ this.productAccountAssignments = productAccountAssignments;
+ this.caseAccountAssignments = caseAccountAssignments;
+ this.oneTimeAccountAssignments = oneTimeAccountAssignments;
}
private Stream<AccountAssignment> allAccountAssignmentsAsStream() {
@@ -49,13 +61,36 @@
private Stream<AccountAssignment> fixedAccountAssignmentsAsStream() {
return Stream.concat(caseAccountAssignments.stream().map(CaseMapper::mapAccountAssignmentEntity),
- productAccountAssignments.stream().map(ProductMapper::mapAccountAssignmentEntity));
+ productAccountAssignmentsAsStream());
+ }
+
+ private Stream<AccountAssignment> productAccountAssignmentsAsStream() {
+ return productAccountAssignments.stream().map(ProductMapper::mapAccountAssignmentEntity);
+ }
+
+ private Optional<AccountAssignment> mapToAccountAssignment(final @Nonnull String accountDesignator) {
+ return allAccountAssignmentsAsStream()
+ .filter(x -> x.getDesignator().equals(accountDesignator))
+ .findFirst();
+ }
+
+ private Optional<AccountAssignment> mapToProductAccountAssignment(final @Nonnull String accountDesignator) {
+ return productAccountAssignments.stream().map(ProductMapper::mapAccountAssignmentEntity)
+ .filter(x -> x.getDesignator().equals(accountDesignator))
+ .findFirst();
+ }
+
+ Optional<AccountAssignment> mapToCaseAccountAssignment(final @Nonnull String accountDesignator) {
+ return caseAccountAssignments.stream().map(CaseMapper::mapAccountAssignmentEntity)
+ .filter(x -> x.getDesignator().equals(accountDesignator))
+ .findFirst();
}
public Optional<String> map(final @Nonnull String accountDesignator) {
- return allAccountAssignmentsAsStream()
- .filter(x -> x.getDesignator().equals(accountDesignator))
- .findFirst()
+ final Set<String> accountAssignmentGroups = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentGroups();
+ if (accountAssignmentGroups.contains(accountDesignator))
+ return Optional.empty();
+ return mapToAccountAssignment(accountDesignator)
.map(AccountAssignment::getAccountIdentifier);
}
@@ -64,9 +99,109 @@
ServiceException.badRequest("A required account designator was not set ''{0}''.", accountDesignator));
}
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ public static class GroupNeedingLedger {
+ final String groupName;
+ final String parentLedger;
+
+ GroupNeedingLedger(final String groupName, final String parentLedger) {
+ this.groupName = groupName;
+ this.parentLedger = parentLedger;
+ }
+
+ public String getGroupName() {
+ return groupName;
+ }
+
+ public String getParentLedger() {
+ return parentLedger;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ GroupNeedingLedger that = (GroupNeedingLedger) o;
+ return Objects.equals(groupName, that.groupName) &&
+ Objects.equals(parentLedger, that.parentLedger);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(groupName, parentLedger);
+ }
+
+ @Override
+ public String toString() {
+ return "GroupNeedingLedger{" +
+ "groupName='" + groupName + '\'' +
+ ", parentLedger='" + parentLedger + '\'' +
+ '}';
+ }
+ }
+
+ public Stream<GroupNeedingLedger> getGroupsNeedingLedgers() {
+ //If all of the accounts in one group are assigned the same ledger, create a grouping ledger at the case level for
+ // those accounts under that ledger.
+ //Save that grouping ledger to an account assignment using the group name as its designator.
+ //To this end, return a stream of group names requiring a ledger, and the parent ledger under which the ledger
+ // should be created.
+
+ final Set<String> accountAssignmentGroups = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentGroups();
+ final Set<RequiredAccountAssignment> accountAssignmentsRequired = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentsRequired();
+
+ return accountAssignmentGroups.stream()
+ .filter(groupName -> !mapToProductAccountAssignment(groupName).isPresent()) //Only assign groups to ledgers which aren't already assigned.
+ .map(groupName -> {
+ final Stream<RequiredAccountAssignment> requiredAccountAssignmentsInThisGroup
+ = accountAssignmentsRequired.stream().filter(x -> groupName.equals(x.getGroup()));
+ final List<String> ledgersAssignedToThem = requiredAccountAssignmentsInThisGroup
+ .map(requiredAccountAssignment -> mapToProductAccountAssignment(requiredAccountAssignment.getAccountDesignator()))
+ .map(optionalAccountAssignment -> optionalAccountAssignment.map(AccountAssignment::getLedgerIdentifier))
+ .distinct()
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .limit(2) //If there's more than one then we won't be creating this ledger. We don't care about more than two.
+ .collect(Collectors.toList());
+ if (ledgersAssignedToThem.size() == 1) {
+ //noinspection ConstantConditions
+ return new GroupNeedingLedger(groupName, ledgersAssignedToThem.get(0));
+ }
+ else
+ return null;
+ })
+ .filter(Objects::nonNull);
+ }
+
public Stream<AccountAssignment> getLedgersNeedingAccounts() {
- return fixedAccountAssignmentsAsStream()
- .filter(x -> !x.getDesignator().equals(AccountDesignators.ENTRY))
- .filter(x -> (x.getAccountIdentifier() == null) && (x.getLedgerIdentifier() != null));
+ final Set<String> accountAssignmentGroups = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentGroups();
+ final Set<RequiredAccountAssignment> accountAssignmentsRequired = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentsRequired();
+ final Map<String, RequiredAccountAssignment> accountAssignmentsRequiredMap = accountAssignmentsRequired.stream().collect(Collectors.toMap(RequiredAccountAssignment::getAccountDesignator, x -> x));
+ final Map<String, Optional<String>> groupToLedgerMapping = accountAssignmentGroups.stream()
+ .collect(Collectors.toMap(
+ Function.identity(),
+ group -> mapToCaseAccountAssignment(group).map(AccountAssignment::getAccountIdentifier)));
+
+ final Stream<AccountAssignment> ledgerAccountAssignments = productAccountAssignmentsAsStream()
+ .filter(x -> !x.getDesignator().equals(AccountDesignators.ENTRY))
+ .filter(x -> (x.getAccountIdentifier() == null) && (x.getLedgerIdentifier() != null));
+
+ return ledgerAccountAssignments
+ .map(ledgerAccountAssignment -> {
+ final String accountAssignmentGroup = accountAssignmentsRequiredMap.get(ledgerAccountAssignment.getDesignator()).getGroup();
+ if (accountAssignmentGroup == null)
+ return ledgerAccountAssignment;
+ else {
+ final Optional<String> changedLedger = groupToLedgerMapping.get(accountAssignmentGroup);
+ if (!changedLedger.isPresent())
+ return ledgerAccountAssignment;
+ else {
+ final AccountAssignment ret = new AccountAssignment();
+ ret.setDesignator(ledgerAccountAssignment.getDesignator());
+ ret.setLedgerIdentifier(changedLedger.get());
+ return ret;
+ }
+ }
+ });
}
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/PaymentBuilder.java b/service/src/main/java/io/mifos/individuallending/internal/service/PaymentBuilder.java
index f158dee..feaabb1 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/PaymentBuilder.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/PaymentBuilder.java
@@ -16,11 +16,13 @@
package io.mifos.individuallending.internal.service;
import com.google.common.collect.Sets;
+import io.mifos.individuallending.IndividualLendingPatternFactory;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.CostComponent;
import io.mifos.portfolio.api.v1.domain.Payment;
+import io.mifos.portfolio.api.v1.domain.RequiredAccountAssignment;
import io.mifos.portfolio.service.internal.util.ChargeInstance;
import java.math.BigDecimal;
@@ -107,26 +109,18 @@
final ChargeDefinition chargeDefinition,
final BigDecimal chargeAmount) {
BigDecimal adjustedChargeAmount = BigDecimal.ZERO;
- if (this.accrualAccounting) {
- if (chargeIsAccrued(chargeDefinition)) {
- if (Action.valueOf(chargeDefinition.getAccrueAction()) == action) {
- final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getFromAccountDesignator(), chargeAmount);
- adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getAccrualAccountDesignator(), maxDebit);
-
- this.addToBalance(chargeDefinition.getFromAccountDesignator(), adjustedChargeAmount.negate());
- this.addToBalance(chargeDefinition.getAccrualAccountDesignator(), adjustedChargeAmount);
- } else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
- final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getAccrualAccountDesignator(), chargeAmount);
- adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getToAccountDesignator(), maxDebit);
-
- this.addToBalance(chargeDefinition.getAccrualAccountDesignator(), adjustedChargeAmount.negate());
- this.addToBalance(chargeDefinition.getToAccountDesignator(), adjustedChargeAmount);
- }
- } else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
+ if (this.accrualAccounting && chargeIsAccrued(chargeDefinition)) {
+ if (Action.valueOf(chargeDefinition.getAccrueAction()) == action) {
final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getFromAccountDesignator(), chargeAmount);
- adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getToAccountDesignator(), maxDebit);
+ adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getAccrualAccountDesignator(), maxDebit);
this.addToBalance(chargeDefinition.getFromAccountDesignator(), adjustedChargeAmount.negate());
+ this.addToBalance(chargeDefinition.getAccrualAccountDesignator(), adjustedChargeAmount);
+ } else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
+ final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getAccrualAccountDesignator(), chargeAmount);
+ adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getToAccountDesignator(), maxDebit);
+
+ this.addToBalance(chargeDefinition.getAccrualAccountDesignator(), adjustedChargeAmount.negate());
this.addToBalance(chargeDefinition.getToAccountDesignator(), adjustedChargeAmount);
}
}
@@ -179,9 +173,30 @@
if (chargeDefinition.getAccrualAccountDesignator() != null)
accountsToCompare.add(chargeDefinition.getAccrualAccountDesignator());
- return !Sets.intersection(accountsToCompare, forAccountDesignators).isEmpty();
+ final Set<String> expandedForAccountDesignators = expandAccountDesignators(forAccountDesignators);
+
+ return !Sets.intersection(accountsToCompare, expandedForAccountDesignators).isEmpty();
}
+ static Set<String> expandAccountDesignators(final Set<String> accountDesignators) {
+ final Set<RequiredAccountAssignment> accountAssignmentsRequired = IndividualLendingPatternFactory.individualLendingPattern().getAccountAssignmentsRequired();
+ final Map<String, List<RequiredAccountAssignment>> accountAssignmentsByGroup = accountAssignmentsRequired.stream()
+ .filter(x -> x.getGroup() != null)
+ .collect(Collectors.groupingBy(RequiredAccountAssignment::getGroup, Collectors.toList()));
+ final Set<String> groupExpansions = accountDesignators.stream()
+ .flatMap(accountDesignator -> {
+ final List<RequiredAccountAssignment> group = accountAssignmentsByGroup.get(accountDesignator);
+ if (group != null)
+ return group.stream();
+ else
+ return Stream.empty();
+ })
+ .map(RequiredAccountAssignment::getAccountDesignator)
+ .collect(Collectors.toSet());
+ final Set<String> ret = new HashSet<>(accountDesignators);
+ ret.addAll(groupExpansions);
+ return ret;
+ }
private static CostComponent constructEmptyCostComponent(final ChargeDefinition chargeDefinition) {
final CostComponent ret = new CostComponent();
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/RealRunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/RealRunningBalances.java
index 761c963..34a6227 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/RealRunningBalances.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/RealRunningBalances.java
@@ -28,13 +28,12 @@
* @author Myrle Krantz
*/
public class RealRunningBalances implements RunningBalances {
- private final ExpiringMap<String, BigDecimal> realBalanceCache;
-
+ private final ExpiringMap<String, BigDecimal> realAccountBalanceCache;
RealRunningBalances(
final AccountingAdapter accountingAdapter,
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
- this.realBalanceCache = ExpiringMap.builder()
+ this.realAccountBalanceCache = ExpiringMap.builder()
.maxSize(20)
.expirationPolicy(ExpirationPolicy.CREATED)
.expiration(30,TimeUnit.SECONDS)
@@ -46,13 +45,13 @@
else {
accountIdentifier = Optional.of(designatorToAccountIdentifierMapper.mapOrThrow(accountDesignator));
}
- return accountIdentifier.map(accountingAdapter::getCurrentBalance).orElse(BigDecimal.ZERO);
+ return accountIdentifier.map(accountingAdapter::getCurrentAccountBalance).orElse(BigDecimal.ZERO);
})
.build();
}
@Override
- public BigDecimal getBalance(final String accountDesignator) {
- return realBalanceCache.get(accountDesignator);
+ public BigDecimal getAccountBalance(final String accountDesignator) {
+ return realAccountBalanceCache.get(accountDesignator);
}
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/RunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/RunningBalances.java
index 08197f6..1a08a7a 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/RunningBalances.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/RunningBalances.java
@@ -15,7 +15,10 @@
*/
package io.mifos.individuallending.internal.service;
+import io.mifos.individuallending.IndividualLendingPatternFactory;
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.portfolio.api.v1.domain.Pattern;
+import io.mifos.portfolio.api.v1.domain.RequiredAccountAssignment;
import java.math.BigDecimal;
import java.util.HashMap;
@@ -29,7 +32,9 @@
final BigDecimal negative = BigDecimal.valueOf(-1);
final BigDecimal positive = BigDecimal.valueOf(1);
- this.put(AccountDesignators.CUSTOMER_LOAN, negative);
+ this.put(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, negative);
+ this.put(AccountDesignators.CUSTOMER_LOAN_FEES, negative);
+ this.put(AccountDesignators.CUSTOMER_LOAN_INTEREST, negative);
this.put(AccountDesignators.LOAN_FUNDS_SOURCE, negative);
this.put(AccountDesignators.PROCESSING_FEE_INCOME, positive);
this.put(AccountDesignators.ORIGINATION_FEE_INCOME, positive);
@@ -42,7 +47,24 @@
this.put(AccountDesignators.ENTRY, positive);
}};
- BigDecimal getBalance(final String accountDesignator);
+ BigDecimal getAccountBalance(final String accountDesignator);
+
+ default BigDecimal getLedgerBalance(final String ledgerDesignator) {
+ final Pattern individualLendingPattern = IndividualLendingPatternFactory.individualLendingPattern();
+ return individualLendingPattern.getAccountAssignmentsRequired().stream()
+ .filter(requiredAccountAssignment -> ledgerDesignator.equals(requiredAccountAssignment.getGroup()))
+ .map(RequiredAccountAssignment::getAccountDesignator)
+ .map(this::getAccountBalance)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+ default BigDecimal getBalance(final String designator) {
+ final Pattern individualLendingPattern = IndividualLendingPatternFactory.individualLendingPattern();
+ if (individualLendingPattern.getAccountAssignmentGroups().contains(designator))
+ return getLedgerBalance(designator);
+ else
+ return getAccountBalance(designator);
+ }
default BigDecimal getMaxDebit(final String accountDesignator, final BigDecimal amount) {
if (accountDesignator.equals(AccountDesignators.ENTRY))
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/SimulatedRunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/SimulatedRunningBalances.java
index 3c4ce83..9c96574 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/SimulatedRunningBalances.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/SimulatedRunningBalances.java
@@ -30,7 +30,8 @@
this.balances = new HashMap<>();
}
- public BigDecimal getBalance(final String accountDesignator) {
+ @Override
+ public BigDecimal getAccountBalance(final String accountDesignator) {
return balances.getOrDefault(accountDesignator, BigDecimal.ZERO);
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
index cff6143..fba4800 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
@@ -104,7 +104,7 @@
return true;
case PROCESSING_FEE_ID:
return true;
- case REPAYMENT_ID:
+ case REPAY_PRINCIPAL_ID:
return false;
default:
return false;
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 df851db..d305e43 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
@@ -28,6 +28,7 @@
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.service.ServiceConstants;
import org.apache.commons.lang.RandomStringUtils;
+import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -49,6 +50,7 @@
@Component
public class AccountingAdapter {
+
public enum IdentifierType {LEDGER, ACCOUNT}
private final LedgerManager ledgerManager;
@@ -153,7 +155,11 @@
return Optional.of(ret);
}
- public BigDecimal getCurrentBalance(final String accountIdentifier) {
+ public BigDecimal getTotalOfCurrentAccountBalances(final String... accountIdentifiers) {
+ return Arrays.stream(accountIdentifiers).map(this::getCurrentAccountBalance).reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+ public BigDecimal getCurrentAccountBalance(final String accountIdentifier) {
try {
final Account account = ledgerManager.findAccount(accountIdentifier);
if (account == null)
@@ -164,6 +170,24 @@
throw ServiceException.internalError("Could not find the account with identifier ''{0}''", accountIdentifier);
}
}
+ public String createLedger(final String customerIdentifier, final String groupName, final String parentLedger) {
+ final Ledger ledger = ledgerManager.findLedger(parentLedger);
+ final List<Ledger> subLedgers = ledger.getSubLedgers() == null ? Collections.emptyList() : ledger.getSubLedgers();
+
+ final Ledger generatedLedger = new Ledger();
+ generatedLedger.setShowAccountsInChart(true);
+ generatedLedger.setParentLedgerIdentifier(parentLedger);
+ generatedLedger.setType(ledger.getType());
+ final String ledgerIdentifer = createLedgerIdentifier(customerIdentifier, groupName, subLedgers);
+ generatedLedger.setIdentifier(ledgerIdentifer);
+ generatedLedger.setDescription("Individual loan case specific ledger");
+ generatedLedger.setName(ledgerIdentifer);
+
+ logger.info("Creating ledger with identifier '{}'", ledgerIdentifer);
+
+ ledgerManager.createLedger(generatedLedger); //TODO: wait?
+ return ledgerIdentifer;
+ }
public String createAccountForLedgerAssignment(final String customerIdentifier, final AccountAssignment ledgerAssignment) {
final Ledger ledger = ledgerManager.findLedger(ledgerAssignment.getLedgerIdentifier());
@@ -197,8 +221,24 @@
customerIdentifier, ledgerAssignment.getDesignator(), ledgerAssignment.getLedgerIdentifier()));
}
+ private String createLedgerIdentifier(
+ final String customerIdentifier,
+ final String groupName,
+ final List<Ledger> subLedgers) {
+ final String partialCustomerIdentifer = StringUtils.left(customerIdentifier, 22);
+ final String partialGroupName = StringUtils.left(groupName, 3);
+ final Set<String> subLedgerIdentifiers = subLedgers.stream().map(Ledger::getIdentifier).collect(Collectors.toSet());
+ long index = 0;
+ while (true) {
+ index++;
+ final String generatedIdentifier = partialCustomerIdentifer + "." + partialGroupName + "." + String.format("%05d", index);
+ if (!subLedgerIdentifiers.contains(generatedIdentifier))
+ return generatedIdentifier;
+ }
+ }
+
private String createAccountNumber(final String customerIdentifier, final String designator, final long accountIndex) {
- return customerIdentifier + "." + designator
+ return StringUtils.left(customerIdentifier, 22) + "." + StringUtils.left(designator, 3)
+ "." + String.format("%05d", accountIndex);
}
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
index 4a639c9..06931c7 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java
@@ -108,7 +108,7 @@
@Test
public void getAmountProportionalTo() {
final SimulatedRunningBalances runningBalances = new SimulatedRunningBalances();
- runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN, testCase.runningBalance.negate());
+ runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, testCase.runningBalance.negate());
final BigDecimal amount = CostComponentService.getAmountProportionalTo(
testCase.chargeProportionalDesignator,
testCase.maximumBalance,
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapperTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapperTest.java
new file mode 100644
index 0000000..be8a427
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapperTest.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.portfolio.api.v1.domain.AccountAssignment;
+import io.mifos.portfolio.service.internal.repository.CaseAccountAssignmentEntity;
+import io.mifos.portfolio.service.internal.repository.ProductAccountAssignmentEntity;
+import io.mifos.portfolio.service.internal.util.AccountingAdapter;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author Myrle Krantz
+ */
+@RunWith(Parameterized.class)
+public class DesignatorToAccountIdentifierMapperTest {
+
+
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ static private class TestCase {
+ final String description;
+ Set<ProductAccountAssignmentEntity> productAccountAssignments;
+ Set<CaseAccountAssignmentEntity> caseAccountAssignments;
+ List<AccountAssignment> oneTimeAccountAssignments;
+ Set<AccountAssignment> expectedLedgersNeedingAccounts;
+ Optional<AccountAssignment> expectedCaseAccountAssignmentMappingForCustomerLoanGroup;
+ Set<DesignatorToAccountIdentifierMapper.GroupNeedingLedger> expectedGroupsNeedingLedgers;
+ Optional<String> expectedMapCustomerLoanPrincipalResult = Optional.empty();
+
+ private TestCase(String description) {
+ this.description = description;
+ }
+
+ TestCase productAccountAssignments(Set<ProductAccountAssignmentEntity> newVal) {
+ this.productAccountAssignments = newVal;
+ return this;
+ }
+
+ TestCase caseAccountAssignments(Set<CaseAccountAssignmentEntity> newVal) {
+ this.caseAccountAssignments = newVal;
+ return this;
+ }
+
+ TestCase oneTimeAccountAssignments(List<AccountAssignment> newVal) {
+ this.oneTimeAccountAssignments = newVal;
+ return this;
+ }
+
+ TestCase expectedLedgersNeedingAccounts(Set<AccountAssignment> newVal) {
+ this.expectedLedgersNeedingAccounts = newVal;
+ return this;
+ }
+
+ TestCase expectedCaseAccountAssignmentMappingForCustomerLoanGroup(Optional<AccountAssignment> newVal) {
+ this.expectedCaseAccountAssignmentMappingForCustomerLoanGroup = newVal;
+ return this;
+ }
+
+ TestCase expectedGroupsNeedingLedgers(Set<DesignatorToAccountIdentifierMapper.GroupNeedingLedger> newVal) {
+ this.expectedGroupsNeedingLedgers = newVal;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "TestCase{" +
+ "description='" + description + '\'' +
+ '}';
+ }
+ }
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<TestCase> ret = new ArrayList<>();
+ final TestCase groupedTestCase = new TestCase("basic grouped customer loan assignments")
+ .productAccountAssignments(new HashSet<>(Arrays.asList(
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, "x"),
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_INTEREST, "x"),
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_FEES, "x")
+ )))
+ .caseAccountAssignments(new HashSet<>(Collections.singletonList(
+ cAssignLedger(AccountDesignators.CUSTOMER_LOAN_GROUP)
+ )))
+ .oneTimeAccountAssignments(Collections.emptyList())
+ .expectedLedgersNeedingAccounts(new HashSet<>(Arrays.asList(
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, "y"),
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_INTEREST, "y"),
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_FEES, "y"))))
+ .expectedCaseAccountAssignmentMappingForCustomerLoanGroup(
+ Optional.of(assignAccount(AccountDesignators.CUSTOMER_LOAN_GROUP)))
+ .expectedGroupsNeedingLedgers(new HashSet<>(Collections.singletonList(
+ new DesignatorToAccountIdentifierMapper.GroupNeedingLedger(AccountDesignators.CUSTOMER_LOAN_GROUP, "x"))));
+ ret.add(groupedTestCase);
+
+ final TestCase groupingIgnoredTestCase = new TestCase("customer loan assignments with ignored grouping")
+ .productAccountAssignments(new HashSet<>(Arrays.asList(
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, "x"),
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_INTEREST, "y"),
+ pAssignLedger(AccountDesignators.CUSTOMER_LOAN_FEES, "z")
+ )))
+ .caseAccountAssignments(Collections.emptySet())
+ .oneTimeAccountAssignments(Collections.emptyList())
+ .expectedLedgersNeedingAccounts(new HashSet<>(Arrays.asList(
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, "x"),
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_INTEREST, "y"),
+ assignLedger(AccountDesignators.CUSTOMER_LOAN_FEES, "z"))))
+ .expectedCaseAccountAssignmentMappingForCustomerLoanGroup(
+ Optional.empty())
+ .expectedGroupsNeedingLedgers(Collections.emptySet());
+ ret.add(groupingIgnoredTestCase);
+ return ret;
+ }
+
+ private static ProductAccountAssignmentEntity pAssignLedger(
+ final String accountDesignator,
+ final String ledgerIdentifier) {
+ final ProductAccountAssignmentEntity ret = new ProductAccountAssignmentEntity();
+ ret.setDesignator(accountDesignator);
+ ret.setIdentifier(ledgerIdentifier);
+ ret.setType(AccountingAdapter.IdentifierType.LEDGER);
+ return ret;
+ }
+
+ private static CaseAccountAssignmentEntity cAssignLedger(
+ final String accountDesignator) {
+ final CaseAccountAssignmentEntity ret = new CaseAccountAssignmentEntity();
+ ret.setDesignator(accountDesignator);
+ ret.setIdentifier("y");
+ return ret;
+ }
+
+ private static AccountAssignment assignLedger(
+ final String accountDesignator,
+ final String ledgerIdentifier) {
+ final AccountAssignment ret = new AccountAssignment();
+ ret.setDesignator(accountDesignator);
+ ret.setLedgerIdentifier(ledgerIdentifier);
+ return ret;
+ }
+
+ private static AccountAssignment assignAccount(
+ final String accountDesignator) {
+ final AccountAssignment ret = new AccountAssignment();
+ ret.setDesignator(accountDesignator);
+ ret.setAccountIdentifier("y");
+ return ret;
+ }
+
+ private final TestCase testCase;
+ private final DesignatorToAccountIdentifierMapper testSubject;
+
+ public DesignatorToAccountIdentifierMapperTest(TestCase testCase) {
+ this.testCase = testCase;
+ this.testSubject = new DesignatorToAccountIdentifierMapper(
+ testCase.productAccountAssignments,
+ testCase.caseAccountAssignments,
+ testCase.oneTimeAccountAssignments);
+ }
+
+ @Test
+ public void map() {
+ Assert.assertEquals(Optional.empty(), testSubject.map(AccountDesignators.CUSTOMER_LOAN_GROUP));
+ Assert.assertEquals(testCase.expectedMapCustomerLoanPrincipalResult, testSubject.map(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL));
+ Assert.assertEquals(Optional.empty(), testSubject.map("this-account-designator-doesnt-exist"));
+ }
+
+ @Test
+ public void mapToCaseAccountAssignment() {
+ final Optional<AccountAssignment> ret = testSubject.mapToCaseAccountAssignment(AccountDesignators.CUSTOMER_LOAN_GROUP);
+ Assert.assertEquals(testCase.expectedCaseAccountAssignmentMappingForCustomerLoanGroup, ret);
+ }
+
+ @Test
+ public void getLedgersNeedingAccounts() {
+ final Set<AccountAssignment> ret = testSubject.getLedgersNeedingAccounts().collect(Collectors.toSet());
+ Assert.assertEquals(testCase.expectedLedgersNeedingAccounts, ret);
+ }
+
+ @Test
+ public void getGroupsNeedingLedgers() {
+ Set<DesignatorToAccountIdentifierMapper.GroupNeedingLedger> ret = testSubject.getGroupsNeedingLedgers().collect(Collectors.toSet());
+ //noinspection ResultOfMethodCallIgnored //Checking GroupNeedingLedger.toString that it doesn't cause exceptions.
+ ret.toString();
+ Assert.assertEquals(testCase.expectedGroupsNeedingLedgers, ret);
+ }
+}
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java b/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
index 13c7ff3..cf7532b 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/Fixture.java
@@ -105,7 +105,7 @@
chargeDefinition.setAccrueAction(Action.APPLY_INTEREST.name());
chargeDefinition.setChargeAction(Action.ACCEPT_PAYMENT.name());
chargeDefinition.setAmount(BigDecimal.ONE);
- chargeDefinition.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN);
+ chargeDefinition.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN_INTEREST);
chargeDefinition.setAccrualAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
chargeDefinition.setToAccountDesignator(AccountDesignators.INTEREST_INCOME);
return new ScheduledCharge(scheduledAction, chargeDefinition, Optional.empty());
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 a31f3e0..8c49177 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
@@ -100,7 +100,7 @@
LOAN_ORIGINATION_FEE_ID,
INTEREST_ID,
DISBURSEMENT_FEE_ID,
- REPAYMENT_ID,
+ REPAY_PRINCIPAL_ID,
DISBURSE_PAYMENT_ID,
LATE_FEE_ID
));
@@ -249,7 +249,9 @@
}
private static List<ChargeDefinition> charges() {
- return IndividualLendingPatternFactory.defaultIndividualLoanCharges();
+ final List<ChargeDefinition> ret = IndividualLendingPatternFactory.requiredIndividualLoanCharges();
+ ret.addAll(IndividualLendingPatternFactory.defaultIndividualLoanCharges());
+ return ret;
}
private static ChargeDefinition getFixedSingleChargeDefinition(
@@ -264,7 +266,7 @@
ret.setChargeAction(action.name());
ret.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
ret.setProportionalTo(null);
- ret.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN);
+ ret.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN_FEES);
ret.setToAccountDesignator(feeAccountDesignator);
ret.setForCycleSizeUnit(null);
return ret;
@@ -316,7 +318,7 @@
.map(x ->
{
final BigDecimal valueOfRepaymentCostComponent = allPlannedPayments.get(x).getPayment().getCostComponents().stream()
- .filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID))
+ .filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.REPAY_PRINCIPAL_ID))
.map(CostComponent::getAmount)
.reduce(BigDecimal::add)
.orElse(BigDecimal.ZERO);
@@ -325,10 +327,13 @@
.map(CostComponent::getAmount)
.reduce(BigDecimal::add)
.orElse(BigDecimal.ZERO);
- final BigDecimal principalDifference = allPlannedPayments.get(x-1).getBalances().get(AccountDesignators.CUSTOMER_LOAN).subtract(allPlannedPayments.get(x).getBalances().get(AccountDesignators.CUSTOMER_LOAN));
+ final BigDecimal principalDifference =
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, x - 1)
+ .subtract(
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, x));
Assert.assertEquals("Checking payment " + x, valueOfRepaymentCostComponent.subtract(valueOfInterestCostComponent), principalDifference);
Assert.assertNotEquals("Remaining principle should always be positive or zero.",
- allPlannedPayments.get(x).getBalances().get(AccountDesignators.CUSTOMER_LOAN).signum(), -1);
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, x).signum(), -1);
final boolean containsLateFee = allPlannedPayments.get(x).getPayment().getCostComponents().stream().anyMatch(y -> y.getChargeIdentifier().equals(LATE_FEE_ID));
Assert.assertFalse("Late fee should not be included in planned payments", containsLateFee);
return valueOfRepaymentCostComponent;
@@ -338,7 +343,7 @@
//All entries should have the correct scale.
allPlannedPayments.forEach(x -> {
x.getPayment().getCostComponents().forEach(y -> Assert.assertEquals(testCase.minorCurrencyUnitDigits, y.getAmount().scale()));
- Assert.assertEquals(testCase.minorCurrencyUnitDigits, x.getBalances().get(AccountDesignators.CUSTOMER_LOAN).scale());
+ Assert.assertEquals(testCase.minorCurrencyUnitDigits, x.getBalances().get(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL).scale());
final int uniqueChargeIdentifierCount = x.getPayment().getCostComponents().stream()
.map(CostComponent::getChargeIdentifier)
.collect(Collectors.toSet())
@@ -347,9 +352,17 @@
x.getPayment().getCostComponents().size(), uniqueChargeIdentifierCount);
});
- Assert.assertEquals("Final balance should be zero.",
+ Assert.assertEquals("Final principal balance should be zero.",
BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
- allPlannedPayments.get(allPlannedPayments.size()-1).getBalances().get(AccountDesignators.CUSTOMER_LOAN));
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, allPlannedPayments.size() - 1));
+
+ Assert.assertEquals("Final interest balance should be zero.",
+ BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_INTEREST, allPlannedPayments.size() - 1));
+
+ Assert.assertEquals("Final fees balance should be zero.",
+ BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
+ getBalanceForPayment(allPlannedPayments, AccountDesignators.CUSTOMER_LOAN_FEES, allPlannedPayments.size() - 1));
//All customer payments should be within one percent of each other.
final Optional<BigDecimal> maxPayment = customerRepayments.stream().max(BigDecimal::compareTo);
@@ -368,6 +381,13 @@
Assert.assertEquals(testCase.expectedChargeIdentifiers, resultChargeIdentifiers);
}
+ private BigDecimal getBalanceForPayment(
+ final List<PlannedPayment> allPlannedPayments,
+ final String accountDesignator,
+ int index) {
+ return allPlannedPayments.get(index).getBalances().get(accountDesignator);
+ }
+
@Test
public void getScheduledCharges() {
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/PaymentBuilderTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/PaymentBuilderTest.java
new file mode 100644
index 0000000..b113d50
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/PaymentBuilderTest.java
@@ -0,0 +1,25 @@
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class PaymentBuilderTest {
+ @Test
+ public void expandAccountDesignators() {
+ final Set<String> ret = PaymentBuilder.expandAccountDesignators(new HashSet<>(Arrays.asList(AccountDesignators.CUSTOMER_LOAN_GROUP, AccountDesignators.ENTRY)));
+ final Set<String> expected = new HashSet<>(Arrays.asList(
+ AccountDesignators.ENTRY,
+ AccountDesignators.CUSTOMER_LOAN_GROUP,
+ AccountDesignators.CUSTOMER_LOAN_PRINCIPAL,
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ AccountDesignators.CUSTOMER_LOAN_INTEREST));
+
+ Assert.assertEquals(expected, ret);
+ }
+
+}
\ No newline at end of file