Finishing up the cost application for loan approval.
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 23d9462..37b337a 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
@@ -41,6 +41,9 @@
String PAYMENT_NAME = "Payment";
String PAYMENT_ID = nameToIdentifier(PAYMENT_NAME);
+ String MAXIMUM_BALANCE_DESIGNATOR = "{maximumbalance}";
+ String RUNNING_BALANCE_DESIGNATOR = "{runningbalance}";
+
static String nameToIdentifier(String name) {
return name.toLowerCase(Locale.US).replace(" ", "-");
}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
index a831129..a7f1ffc 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/ChargeDefinition.java
@@ -30,7 +30,10 @@
* @author Myrle Krantz
*/
@SuppressWarnings({"unused", "WeakerAccess"})
-@ScriptAssert(lang = "javascript", script = "_this.amount !== null && _this.amount.scale() <= 4 && ((_this.accrueAction === null && _this.accrualAccountDesignator === null) || (_this.accrueAction !== null && _this.accrualAccountDesignator !== null))")
+@ScriptAssert(lang = "javascript", script = "_this.amount !== null " +
+ "&& _this.amount.scale() <= 4 " +
+ "&& ((_this.accrueAction === null && _this.accrualAccountDesignator === null) || (_this.accrueAction !== null && _this.accrualAccountDesignator !== null))" +
+ "&& ((_this.chargeMethod == 'PROPORTIONAL' && _this.proportionalTo !== null) || (_this.chargeMethod == 'FIXED' && _this.proportionalTo === null))")
public class ChargeDefinition {
@SuppressWarnings("WeakerAccess")
public enum ChargeMethod {
@@ -58,6 +61,9 @@
@NotNull
private ChargeMethod chargeMethod;
+ @ValidIdentifier(optional = true)
+ private String proportionalTo;
+
@ValidIdentifier
private String fromAccountDesignator; //Where it's going.
@@ -131,6 +137,14 @@
this.chargeMethod = chargeMethod;
}
+ public String getProportionalTo() {
+ return proportionalTo;
+ }
+
+ public void setProportionalTo(String proportionalTo) {
+ this.proportionalTo = proportionalTo;
+ }
+
public String getFromAccountDesignator() {
return fromAccountDesignator;
}
@@ -176,6 +190,7 @@
Objects.equals(chargeAction, that.chargeAction) &&
Objects.equals(amount, that.amount) &&
chargeMethod == that.chargeMethod &&
+ Objects.equals(proportionalTo, that.proportionalTo) &&
Objects.equals(fromAccountDesignator, that.fromAccountDesignator) &&
Objects.equals(accrualAccountDesignator, that.accrualAccountDesignator) &&
Objects.equals(toAccountDesignator, that.toAccountDesignator) &&
@@ -184,7 +199,7 @@
@Override
public int hashCode() {
- return Objects.hash(identifier, name, description, accrueAction, chargeAction, amount, chargeMethod, fromAccountDesignator, accrualAccountDesignator, toAccountDesignator, forCycleSizeUnit);
+ return Objects.hash(identifier, name, description, accrueAction, chargeAction, amount, chargeMethod, proportionalTo, fromAccountDesignator, accrualAccountDesignator, toAccountDesignator, forCycleSizeUnit);
}
@Override
@@ -197,6 +212,7 @@
", chargeAction='" + chargeAction + '\'' +
", amount=" + amount +
", chargeMethod=" + chargeMethod +
+ ", proportionalTo='" + proportionalTo + '\'' +
", fromAccountDesignator='" + fromAccountDesignator + '\'' +
", accrualAccountDesignator='" + accrualAccountDesignator + '\'' +
", toAccountDesignator='" + toAccountDesignator + '\'' +
diff --git a/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java b/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java
index bb4df4a..0838319 100644
--- a/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java
+++ b/api/src/test/java/io/mifos/portfolio/api/v1/domain/ChargeDefinitionTest.java
@@ -18,6 +18,7 @@
import io.mifos.core.test.domain.ValidationTest;
import io.mifos.core.test.domain.ValidationTestCase;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import org.apache.commons.lang.RandomStringUtils;
import org.junit.runners.Parameterized;
import java.math.BigDecimal;
@@ -42,6 +43,7 @@
ret.setChargeAction(Action.OPEN.name());
ret.setAmount(BigDecimal.ONE);
ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ ret.setProportionalTo("balance");
ret.setFromAccountDesignator("x1234567898");
ret.setToAccountDesignator("y1234567898");
ret.setForCycleSizeUnit(ChronoUnit.YEARS);
@@ -86,7 +88,21 @@
ret.add(new ValidationTestCase<ChargeDefinition>("nullChargeMethod")
.adjustment(x -> x.setChargeMethod(null))
.valid(false));
+ ret.add(new ValidationTestCase<ChargeDefinition>("invalidProportionalToIdentifier")
+ .adjustment(x -> x.setProportionalTo(RandomStringUtils.random(33)))
+ .valid(false));
+ ret.add(new ValidationTestCase<ChargeDefinition>("missingProportionalToIdentifierOnProportionalCharge")
+ .adjustment(x -> x.setProportionalTo(null))
+ .valid(false));
+ ret.add(new ValidationTestCase<ChargeDefinition>("presentProportionalToIdentifierOnFixedCharge")
+ .adjustment(x -> x.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED))
+ .valid(false));
+ ret.add(new ValidationTestCase<ChargeDefinition>("missingProportionalToIdentifierOnFixedCharge")
+ .adjustment(x -> {
+ x.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+ x.setProportionalTo(null);
+ })
+ .valid(true));
return ret;
}
-
}
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 3c401f3..c5d87fb 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -17,15 +17,17 @@
import io.mifos.accounting.api.v1.client.LedgerManager;
import io.mifos.accounting.api.v1.domain.*;
+import org.hamcrest.Description;
+import org.mockito.AdditionalMatchers;
import org.mockito.ArgumentMatcher;
import org.mockito.Mockito;
import javax.validation.Validation;
import javax.validation.Validator;
import java.math.BigDecimal;
-import java.util.Optional;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.Set;
-import java.util.stream.Collectors;
import static io.mifos.portfolio.Fixture.*;
import static org.mockito.Matchers.argThat;
@@ -106,6 +108,52 @@
return ret;
}
+ private static AccountPage customerLoanAccountsPage() {
+ final Account customerLoanAccount1 = new Account();
+ customerLoanAccount1.setIdentifier("customerLoanAccount1");
+ final Account customerLoanAccount2 = new Account();
+ customerLoanAccount2.setIdentifier("customerLoanAccount2");
+ final Account customerLoanAccount3 = new Account();
+ customerLoanAccount3.setIdentifier("customerLoanAccount3");
+
+ final AccountPage ret = new AccountPage();
+ ret.setTotalElements(3L);
+ ret.setTotalPages(1);
+ ret.setAccounts(Arrays.asList(customerLoanAccount1, customerLoanAccount2, customerLoanAccount3));
+ return ret;
+ }
+
+ private static Object pendingDisbursalAccountsPage() {
+ final Account pendingDisbursalAccount1 = new Account();
+ pendingDisbursalAccount1.setIdentifier("pendingDisbursalAccount1");
+ final Account pendingDisbursalAccount2 = new Account();
+ pendingDisbursalAccount2.setIdentifier("pendingDisbursalAccount2");
+ final Account pendingDisbursalAccount3 = new Account();
+ pendingDisbursalAccount3.setIdentifier("pendingDisbursalAccount3");
+
+ final AccountPage ret = new AccountPage();
+ ret.setTotalElements(3L);
+ ret.setTotalPages(1);
+ ret.setAccounts(Arrays.asList(pendingDisbursalAccount1, pendingDisbursalAccount2, pendingDisbursalAccount3));
+ return ret;
+ }
+
+ private static <T> Valid<T> isValid() {
+ return new Valid<>();
+ }
+
+ private static class Valid<T> extends ArgumentMatcher<T> {
+ @Override
+ public boolean matches(final Object argument) {
+ if (argument == null)
+ return false;
+ final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+ final Set errors = validator.validate(argument);
+
+ return errors.size() == 0;
+ }
+ }
+
private static class AccountMatcher extends ArgumentMatcher<Account> {
private final String ledgerIdentifer;
private final AccountType type;
@@ -126,11 +174,7 @@
checkedArgument = (Account) argument;
- final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
- final Set errors = validator.validate(checkedArgument, Account.class);
-
- return errors.size() == 0 &&
- checkedArgument.getLedger().equals(ledgerIdentifer) &&
+ return checkedArgument.getLedger().equals(ledgerIdentifer) &&
checkedArgument.getType().equals(type.name()) &&
checkedArgument.getBalance() == 0.0;
}
@@ -141,17 +185,14 @@
}
private static class JournalEntryMatcher extends ArgumentMatcher<JournalEntry> {
- private final String expectedFromAccountIdentifier;
- private final String expectedToAccountIdentifier;
- private final BigDecimal expectedAmount;
+ private final Set<Debtor> debtors;
+ private final Set<Creditor> creditors;
private JournalEntry checkedArgument;
- private JournalEntryMatcher(final String expectedFromAccountIdentifier,
- final String expectedToAccountIdentifier,
- final BigDecimal amount) {
- this.expectedFromAccountIdentifier = expectedFromAccountIdentifier;
- this.expectedToAccountIdentifier = expectedToAccountIdentifier;
- this.expectedAmount = amount;
+ private JournalEntryMatcher(final Set<Debtor> debtors,
+ final Set<Creditor> creditors) {
+ this.debtors = debtors;
+ this.creditors = creditors;
this.checkedArgument = null; //Set when matches called.
}
@@ -163,37 +204,24 @@
return false;
checkedArgument = (JournalEntry) argument;
- final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
- final Set errors = validator.validate(checkedArgument);
- final Double debitAmount = checkedArgument.getDebtors().stream()
- .collect(Collectors.summingDouble(x -> Double.valueOf(x.getAmount())));
+ checkedArgument.getDebtors();
+ checkedArgument.getCreditors();
- final Optional<String> fromAccountIdentifier = checkedArgument.getDebtors().stream().findFirst().map(Debtor::getAccountNumber);
-
- final Double creditAmount = checkedArgument.getCreditors().stream()
- .collect(Collectors.summingDouble(x -> Double.valueOf(x.getAmount())));
-
- final Optional<String> toAccountIdentifier = checkedArgument.getCreditors().stream().findFirst().map(Creditor::getAccountNumber);
-
- return (errors.size() == 0 &&
- fromAccountIdentifier.isPresent() && fromAccountIdentifier.get().equals(expectedFromAccountIdentifier) &&
- toAccountIdentifier.isPresent() && toAccountIdentifier.get().equals(expectedToAccountIdentifier) &&
- creditAmount.equals(debitAmount) &&
- creditAmount.equals(expectedAmount.doubleValue()));
+ return this.debtors.equals(checkedArgument.getDebtors()) &&
+ this.creditors.equals(checkedArgument.getCreditors());
}
- JournalEntry getCheckedArgument() {
- return checkedArgument;
+ @Override
+ public void describeTo(final Description description) {
+ description.appendText(this.toString());
}
@Override
public String toString() {
return "JournalEntryMatcher{" +
- "expectedFromAccountIdentifier='" + expectedFromAccountIdentifier + '\'' +
- ", expectedToAccountIdentifier='" + expectedToAccountIdentifier + '\'' +
- ", expectedAmount=" + expectedAmount +
- ", checkedArgument=" + checkedArgument +
+ "debtors=" + debtors +
+ ", creditors=" + creditors +
'}';
}
}
@@ -208,13 +236,17 @@
Mockito.doReturn(loanOriginationFeesIncomeAccount()).when(ledgerManagerMock).findAccount(LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER);
Mockito.doReturn(processingFeeIncomeAccount()).when(ledgerManagerMock).findAccount(PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER);
Mockito.doReturn(tellerOneAccount()).when(ledgerManagerMock).findAccount(TELLER_ONE_ACCOUNT_IDENTIFIER);
+ Mockito.doReturn(customerLoanAccountsPage()).when(ledgerManagerMock).fetchAccountsOfLedger(Mockito.eq(CUSTOMER_LOAN_LEDGER_IDENTIFIER),
+ Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
+ Mockito.doReturn(pendingDisbursalAccountsPage()).when(ledgerManagerMock).fetchAccountsOfLedger(Mockito.eq(PENDING_DISBURSAL_LEDGER_IDENTIFIER),
+ Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
}
static String verifyAccountCreation(final LedgerManager ledgerManager,
final String ledgerIdentifier,
final AccountType type) {
final AccountMatcher specifiesCorrectAccount = new AccountMatcher(ledgerIdentifier, type);
- Mockito.verify(ledgerManager).createAccount(argThat(specifiesCorrectAccount));
+ Mockito.verify(ledgerManager).createAccount(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectAccount)));
return specifiesCorrectAccount.getCheckedArgument().getIdentifier();
}
@@ -222,7 +254,16 @@
final String fromAccountIdentifier,
final String toAccountIdentifier,
final BigDecimal amount) {
- final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(fromAccountIdentifier, toAccountIdentifier, amount);
- Mockito.verify(ledgerManager).createJournalEntry(argThat(specifiesCorrectJournalEntry));
+ final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(
+ Collections.singleton(new Debtor(fromAccountIdentifier, amount.toPlainString())),
+ Collections.singleton(new Creditor(toAccountIdentifier, amount.toPlainString())));
+ Mockito.verify(ledgerManager).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
}
-}
+
+ static void verifyTransfer(final LedgerManager ledgerManager,
+ final Set<Debtor> debtors,
+ final Set<Creditor> creditors) {
+ final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(debtors, creditors);
+ Mockito.verify(ledgerManager).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
+ }
+}
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/portfolio/Fixture.java b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
index b9f0893..3ad05ee 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -63,7 +63,10 @@
product.setMinorCurrencyUnitDigits(2);
final Set<AccountAssignment> accountAssignments = new HashSet<>();
- accountAssignments.add(new AccountAssignment(PENDING_DISBURSAL, PENDING_DISBURSAL_LEDGER_IDENTIFIER));
+ final AccountAssignment pendingDisbursalAccountAssignment = new AccountAssignment();
+ pendingDisbursalAccountAssignment.setDesignator(PENDING_DISBURSAL);
+ pendingDisbursalAccountAssignment.setLedgerIdentifier(PENDING_DISBURSAL_LEDGER_IDENTIFIER);
+ accountAssignments.add(pendingDisbursalAccountAssignment);
accountAssignments.add(new AccountAssignment(PROCESSING_FEE_INCOME, PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(ORIGINATION_FEE_INCOME, LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(DISBURSEMENT_FEE_INCOME, "001-004"));
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteraction.java b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteraction.java
index c471655..91bf999 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteraction.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteraction.java
@@ -17,6 +17,8 @@
import com.google.gson.Gson;
import io.mifos.accounting.api.v1.domain.AccountType;
+import io.mifos.accounting.api.v1.domain.Creditor;
+import io.mifos.accounting.api.v1.domain.Debtor;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.portfolio.api.v1.domain.*;
@@ -27,6 +29,8 @@
import java.math.BigDecimal;
import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
import static io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants.APPROVE_INDIVIDUALLOAN_CASE;
@@ -46,6 +50,7 @@
final ChargeDefinition processingFee = portfolioManager.getChargeDefinition(product.getIdentifier(), PROCESSING_FEE_ID);
processingFee.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
processingFee.setAmount(BigDecimal.valueOf(10_0000, 4));
+ processingFee.setProportionalTo(null);
portfolioManager.changeChargeDefinition(product.getIdentifier(), PROCESSING_FEE_ID, processingFee);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CHARGE_DEFINITION,
new ChargeDefinitionEvent(product.getIdentifier(), PROCESSING_FEE_ID)));
@@ -53,6 +58,7 @@
final ChargeDefinition loanOriginationFee = portfolioManager.getChargeDefinition(product.getIdentifier(), LOAN_ORIGINATION_FEE_ID);
loanOriginationFee.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
loanOriginationFee.setAmount(BigDecimal.valueOf(100_0000, 4));
+ loanOriginationFee.setProportionalTo(null);
portfolioManager.changeChargeDefinition(product.getIdentifier(), LOAN_ORIGINATION_FEE_ID, loanOriginationFee);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CHARGE_DEFINITION,
new ChargeDefinitionEvent(product.getIdentifier(), LOAN_ORIGINATION_FEE_ID)));
@@ -60,8 +66,6 @@
portfolioManager.enableProduct(product.getIdentifier(), true);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_PRODUCT_ENABLE, product.getIdentifier()));
-
-
//Create case.
final CaseParameters caseParameters = Fixture.createAdjustedCaseParameters(x -> {});
final String caseParametersAsString = new Gson().toJson(caseParameters);
@@ -70,7 +74,7 @@
//Open the case and accept a processing fee.
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN);
checkCostComponentForActionCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN,
- new CostComponent(processingFee.getIdentifier(), processingFee.getAmount()));
+ new CostComponent(processingFee.getIdentifier(), processingFee.getAmount().setScale(product.getMinorCurrencyUnitDigits(), BigDecimal.ROUND_UNNECESSARY)));
final AccountAssignment openCommandProcessingFeeAccountAssignment = new AccountAssignment();
openCommandProcessingFeeAccountAssignment.setDesignator(processingFee.getFromAccountDesignator());
@@ -81,11 +85,41 @@
OPEN_INDIVIDUALLOAN_CASE, Case.State.PENDING);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Action.DENY);
checkCostComponentForActionCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE,
- new CostComponent(loanOriginationFee.getIdentifier(), loanOriginationFee.getAmount()));
+ new CostComponent(loanOriginationFee.getIdentifier(), loanOriginationFee.getAmount().setScale(product.getMinorCurrencyUnitDigits(), BigDecimal.ROUND_UNNECESSARY)),
+ new CostComponent(LOAN_FUNDS_ALLOCATION_ID, caseParameters.getMaximumBalance().setScale(product.getMinorCurrencyUnitDigits(), BigDecimal.ROUND_UNNECESSARY)));
AccountingFixture.verifyTransfer(ledgerManager,
TELLER_ONE_ACCOUNT_IDENTIFIER, PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER,
- processingFee.getAmount()
+ processingFee.getAmount().setScale(product.getMinorCurrencyUnitDigits(), BigDecimal.ROUND_UNNECESSARY)
);
+
+
+ //Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
+ final AccountAssignment approveCommandOriginationFeeAccountAssignment = new AccountAssignment();
+ approveCommandOriginationFeeAccountAssignment.setDesignator(loanOriginationFee.getFromAccountDesignator());
+ approveCommandOriginationFeeAccountAssignment.setAccountIdentifier(TELLER_ONE_ACCOUNT_IDENTIFIER);
+
+ checkStateTransfer(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE,
+ Collections.singletonList(approveCommandOriginationFeeAccountAssignment),
+ APPROVE_INDIVIDUALLOAN_CASE, Case.State.APPROVED);
+ checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Action.CLOSE);
+
+ final String pendingDisbursalAccountIdentifier =
+ AccountingFixture.verifyAccountCreation(ledgerManager, Fixture.PENDING_DISBURSAL_LEDGER_IDENTIFIER, AccountType.ASSET);
+ final String customerLoanAccountIdentifier =
+ AccountingFixture.verifyAccountCreation(ledgerManager, Fixture.CUSTOMER_LOAN_LEDGER_IDENTIFIER, AccountType.ASSET);
+
+ final Set<Debtor> debtors = new HashSet<>();
+ debtors.add(new Debtor(LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER, toString(caseParameters.getMaximumBalance(), product.getMinorCurrencyUnitDigits())));
+ debtors.add(new Debtor(TELLER_ONE_ACCOUNT_IDENTIFIER, toString(loanOriginationFee.getAmount(), product.getMinorCurrencyUnitDigits())));
+
+ final Set<Creditor> creditors = new HashSet<>();
+ creditors.add(new Creditor(pendingDisbursalAccountIdentifier, toString(caseParameters.getMaximumBalance(), product.getMinorCurrencyUnitDigits())));
+ creditors.add(new Creditor(LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER, toString(loanOriginationFee.getAmount(), product.getMinorCurrencyUnitDigits())));
+ AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
}
-}
+
+ private String toString(final BigDecimal bigDecimal, final int scale) {
+ return bigDecimal.setScale(scale, BigDecimal.ROUND_UNNECESSARY).toPlainString();
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index bd4ff15..6591023 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -17,14 +17,17 @@
import com.google.gson.Gson;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
+import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
import io.mifos.individuallending.internal.repository.CaseCreditWorthinessFactorEntity;
import io.mifos.individuallending.internal.repository.CaseParametersEntity;
import io.mifos.individuallending.internal.repository.CaseParametersRepository;
import io.mifos.individuallending.internal.repository.CreditWorthinessFactorType;
+import io.mifos.individuallending.internal.service.CostComponentService;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
import io.mifos.portfolio.api.v1.domain.Pattern;
import io.mifos.portfolio.service.ServiceConstants;
import io.mifos.products.spi.PatternFactory;
@@ -50,16 +53,19 @@
public class IndividualLendingPatternFactory implements PatternFactory {
final static private String INDIVIDUAL_LENDING_PACKAGE = "io.mifos.individuallending.api.v1";
private final CaseParametersRepository caseParametersRepository;
- private IndividualLendingCommandDispatcher individualLendingCommandDispatcher;
+ private final CostComponentService costComponentService;
+ private final IndividualLendingCommandDispatcher individualLendingCommandDispatcher;
private final Gson gson;
@Autowired
IndividualLendingPatternFactory(
final CaseParametersRepository caseParametersRepository,
+ final CostComponentService costComponentService,
final IndividualLendingCommandDispatcher individualLendingCommandDispatcher,
@Qualifier(ServiceConstants.GSON_NAME) final Gson gson)
{
this.caseParametersRepository = caseParametersRepository;
+ this.costComponentService = costComponentService;
this.individualLendingCommandDispatcher = individualLendingCommandDispatcher;
this.gson = gson;
}
@@ -118,15 +124,16 @@
ENTRY,
DISBURSEMENT_FEE_INCOME);
- //TODO: Make proportional to payment rather than loan amount.
+ //TODO: Make payable at time of ACCEPT_PAYMENT but accrued at MARK_LATE
final ChargeDefinition lateFee = charge(
LATE_FEE_NAME,
- Action.ACCEPT_PAYMENT,
+ Action.MARK_LATE,
BigDecimal.valueOf(0.01),
CUSTOMER_LOAN,
LATE_FEE_INCOME);
lateFee.setAccrueAction(Action.MARK_LATE.name());
lateFee.setAccrualAccountDesignator(LATE_FEE_ACCRUAL);
+ lateFee.setProportionalTo(ChargeIdentifiers.PAYMENT_ID);
//TODO: Make multiple write off allowance charges.
final ChargeDefinition writeOffAllowanceCharge = charge(
@@ -135,6 +142,7 @@
BigDecimal.valueOf(0.30),
PENDING_DISBURSAL,
ARREARS_ALLOWANCE);
+ writeOffAllowanceCharge.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR);
final ChargeDefinition interestCharge = charge(
INTEREST_NAME,
@@ -145,6 +153,7 @@
interestCharge.setForCycleSizeUnit(ChronoUnit.YEARS);
interestCharge.setAccrueAction(Action.APPLY_INTEREST.name());
interestCharge.setAccrualAccountDesignator(INTEREST_ACCRUAL);
+ interestCharge.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR);
final ChargeDefinition disbursementReturnCharge = charge(
RETURN_DISBURSEMENT_NAME,
@@ -152,9 +161,11 @@
BigDecimal.valueOf(1.0),
PENDING_DISBURSAL,
LOAN_FUNDS_SOURCE);
+ interestCharge.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR); //TODO: Balance in which account?
ret.add(processingFee);
ret.add(loanOriginationFee);
+ ret.add(loanFundsAllocation);
ret.add(disbursementFee);
ret.add(lateFee);
ret.add(writeOffAllowanceCharge);
@@ -247,6 +258,17 @@
return getAllowedNextActionsForState(state).stream().map(Enum::name).collect(Collectors.toSet());
}
+ @Override
+ public List<CostComponent> getCostComponentsForAction(
+ final String productIdentifier,
+ final String caseIdentifier,
+ final String actionIdentifier) {
+ return costComponentService.getCostComponents(productIdentifier, caseIdentifier, Action.valueOf(actionIdentifier))
+ .stream()
+ .map(x -> new CostComponent(x.getChargeIdentifier(), x.getAmount()))
+ .collect(Collectors.toList());
+ }
+
public static Set<Action> getAllowedNextActionsForState(Case.State state) {
switch (state)
{
@@ -285,6 +307,7 @@
ret.setChargeAction(action.name());
ret.setAmount(defaultAmount);
ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ ret.setProportionalTo(ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR);
ret.setFromAccountDesignator(fromAccount);
ret.setToAccountDesignator(toAccount);
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 4d42435..6f65ccb 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
@@ -22,19 +22,15 @@
import io.mifos.core.command.annotation.EventEmitter;
import io.mifos.core.lang.ServiceException;
import io.mifos.individuallending.IndividualLendingPatternFactory;
-import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
import io.mifos.individuallending.internal.command.*;
-import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
-import io.mifos.individuallending.internal.repository.CaseParametersRepository;
-import io.mifos.individuallending.internal.service.IndividualLoanService;
+import io.mifos.individuallending.internal.service.*;
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.events.EventConstants;
import io.mifos.portfolio.service.internal.mapper.CaseMapper;
-import io.mifos.portfolio.service.internal.mapper.ProductMapper;
import io.mifos.portfolio.service.internal.repository.*;
import io.mifos.portfolio.service.internal.util.AccountingAdapter;
import io.mifos.portfolio.service.internal.util.ChargeInstance;
@@ -45,9 +41,7 @@
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.List;
-import java.util.Set;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* @author Myrle Krantz
@@ -55,94 +49,66 @@
@SuppressWarnings("unused")
@Aggregate
public class IndividualLoanCommandHandler {
-
- private final ProductRepository productRepository;
private final CaseRepository caseRepository;
- private final CaseParametersRepository caseParametersRepository;
- private final AccountingAdapter accountingAdapter;
+ private final CostComponentService costComponentService;
private final IndividualLoanService individualLoanService;
+ private final AccountingAdapter accountingAdapter;
@Autowired
- public IndividualLoanCommandHandler(final ProductRepository productRepository,
- final CaseRepository caseRepository,
- final CaseParametersRepository caseParametersRepository,
- final AccountingAdapter accountingAdapter,
- final IndividualLoanService individualLoanService) {
- this.productRepository = productRepository;
+ public IndividualLoanCommandHandler(
+ final CaseRepository caseRepository,
+ final CostComponentService costComponentService,
+ final IndividualLoanService individualLoanService,
+ final AccountingAdapter accountingAdapter) {
this.caseRepository = caseRepository;
- this.caseParametersRepository = caseParametersRepository;
- this.accountingAdapter = accountingAdapter;
+ this.costComponentService = costComponentService;
this.individualLoanService = individualLoanService;
+ this.accountingAdapter = accountingAdapter;
}
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final OpenCommand command) {
- final ProductEntity product = getProductOrThrow(command.getProductIdentifier());
- final CaseEntity customerCase = getCaseOrThrow(command.getProductIdentifier(), command.getCaseIdentifier());
- checkActionCanBeExecuted(Case.State.valueOf(customerCase.getCurrentState()), Action.OPEN);
+ final String productIdentifier = command.getProductIdentifier();
+ final String caseIdentifier = command.getCaseIdentifier();
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.OPEN);
- final CaseParameters caseParameters =
- caseParametersRepository.findByCaseId(customerCase.getId())
- .map(CaseParametersMapper::mapEntity)
- .orElseThrow(() -> ServiceException.notFound(
- "Individual loan with identifier ''{0}''.''{1}'' doesn''t exist.",
- command.getProductIdentifier(), command.getCaseIdentifier()));
- final Set<ProductAccountAssignmentEntity> productAccountAssignments = product.getAccountAssignments();
- final Set<CaseAccountAssignmentEntity> caseAccountAssignments = customerCase.getAccountAssignments();
+ final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ individualLoanService.getCostComponentsForRepaymentPeriod(productIdentifier, dataContextOfAction.getCaseParameters(), BigDecimal.ZERO, Action.OPEN, today(), LocalDate.now());
- final List<ChargeInstance> chargesNamedViaAccountDesignators =
- individualLoanService.getChargeInstances(command.getProductIdentifier(), caseParameters, BigDecimal.ZERO, Action.OPEN, today(), LocalDate.now());
- final List<ChargeInstance> chargesNamedViaAccountIdentifier = chargesNamedViaAccountDesignators.stream().map(x -> new ChargeInstance(
- designatorToAccountIdentifierOrThrow(x.getFromAccount(), command.getCommand().getOneTimeAccountAssignments(), caseAccountAssignments, productAccountAssignments),
- designatorToAccountIdentifierOrThrow(x.getToAccount(), command.getCommand().getOneTimeAccountAssignments(), caseAccountAssignments, productAccountAssignments),
- x.getAmount())).collect(Collectors.toList());
+
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+
+ final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream().map(x -> new ChargeInstance(
+ designatorToAccountIdentifierMapper.mapOrThrow(x.getKey().getFromAccountDesignator()),
+ designatorToAccountIdentifierMapper.mapOrThrow(x.getKey().getToAccountDesignator()),
+ x.getValue().getAmount())).collect(Collectors.toList());
//TODO: Accrual
- accountingAdapter.bookCharges(chargesNamedViaAccountIdentifier,
+ accountingAdapter.bookCharges(charges,
command.getCommand().getNote(),
- command.getProductIdentifier() + "." + command.getCaseIdentifier() + "." + Action.OPEN.name(),
+ productIdentifier + "." + caseIdentifier + "." + Action.OPEN.name(),
Action.OPEN.getTransactionType());
- //Only move to pending if book charges command was accepted.
- updateCaseState(customerCase, Case.State.PENDING);
+ //Only move to new state if book charges command was accepted.
+ updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.PENDING);
- return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
- }
-
- private static LocalDate today() {
- return LocalDate.now(ZoneId.of("UTC"));
- }
-
- private String designatorToAccountIdentifierOrThrow(final String accountDesignator,
- final List<AccountAssignment> oneTimeAccountAssignments,
- final Set<CaseAccountAssignmentEntity> caseAccountAssignments,
- final Set<ProductAccountAssignmentEntity> productAccountAssignments) {
- return allAccountAssignmentsAsStream(oneTimeAccountAssignments, caseAccountAssignments, productAccountAssignments)
- .filter(x -> x.getDesignator().equals(accountDesignator))
- .findFirst()
- .map(AccountAssignment::getAccountIdentifier)
- .orElseThrow(() -> ServiceException.badRequest("A required account designator was not set ''{0}''.", accountDesignator));
- }
-
- private Stream<AccountAssignment> allAccountAssignmentsAsStream(
- final List<AccountAssignment> oneTimeAccountAssignments,
- final Set<CaseAccountAssignmentEntity> caseAccountAssignments,
- final Set<ProductAccountAssignmentEntity> productAccountAssignments) {
- return Stream.concat(Stream.concat(
- oneTimeAccountAssignments.stream(),
- caseAccountAssignments.stream().map(CaseMapper::mapAccountAssignmentEntity)),
- productAccountAssignments.stream().map(ProductMapper::mapAccountAssignmentEntity));
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, Action.OPEN.name());
}
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.DENY_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final DenyCommand command) {
- final CaseEntity customerCase = getCaseOrThrow(command.getProductIdentifier(), command.getCaseIdentifier());
- checkActionCanBeExecuted(Case.State.valueOf(customerCase.getCurrentState()), Action.DENY);
- updateCaseState(customerCase, Case.State.CLOSED);
+ final String productIdentifier = command.getProductIdentifier();
+ final String caseIdentifier = command.getCaseIdentifier();
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.DENY);
+ updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.CLOSED);
return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
}
@@ -150,19 +116,56 @@
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.APPROVE_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final ApproveCommand command) {
- final CaseEntity customerCase = getCaseOrThrow(command.getProductIdentifier(), command.getCaseIdentifier());
- checkActionCanBeExecuted(Case.State.valueOf(customerCase.getCurrentState()), Action.APPROVE);
- updateCaseState(customerCase, Case.State.APPROVED);
- return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
+ final String productIdentifier = command.getProductIdentifier();
+ final String caseIdentifier = command.getCaseIdentifier();
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.APPROVE);
+
+ //TODO: Check for incomplete task instances.
+
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+
+ //Create the needed account assignments and persist them for the case.
+ designatorToAccountIdentifierMapper.getLedgersNeedingAccounts()
+ .map(ledger ->
+ new AccountAssignment(ledger.getDesignator(),
+ accountingAdapter.createAccountForLedgerAssignment(dataContextOfAction.getCaseParameters().getCustomerIdentifier(), ledger)))
+ .map(accountAssignment -> CaseMapper.map(accountAssignment, dataContextOfAction.getCustomerCase()))
+ .forEach(caseAccountAssignmentEntity ->
+ dataContextOfAction.getCustomerCase().getAccountAssignments().add(caseAccountAssignmentEntity)
+ );
+ caseRepository.save(dataContextOfAction.getCustomerCase());
+
+ //Charge the approval fee if applicable.
+ final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ individualLoanService.getCostComponentsForRepaymentPeriod(productIdentifier, dataContextOfAction.getCaseParameters(), BigDecimal.ZERO, Action.APPROVE, today(), LocalDate.now());
+
+ final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream().map(x -> new ChargeInstance(
+ designatorToAccountIdentifierMapper.mapOrThrow(x.getKey().getFromAccountDesignator()),
+ designatorToAccountIdentifierMapper.mapOrThrow(x.getKey().getToAccountDesignator()),
+ x.getValue().getAmount())).collect(Collectors.toList());
+
+ accountingAdapter.bookCharges(charges,
+ command.getCommand().getNote(),
+ productIdentifier + "." + caseIdentifier + "." + Action.APPROVE.name(),
+ Action.APPROVE.getTransactionType());
+
+ //Only move to new state if book charges command was accepted.
+ updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.APPROVED);
+
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, Action.APPROVE.name());
}
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.DISBURSE_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final DisburseCommand command) {
- final CaseEntity customerCase = getCaseOrThrow(command.getProductIdentifier(), command.getCaseIdentifier());
- checkActionCanBeExecuted(Case.State.valueOf(customerCase.getCurrentState()), Action.DISBURSE);
- updateCaseState(customerCase, Case.State.ACTIVE);
+ final String productIdentifier = command.getProductIdentifier();
+ final String caseIdentifier = command.getCaseIdentifier();
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.DISBURSE);
+ updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.ACTIVE);
return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
}
@@ -170,9 +173,11 @@
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final AcceptPaymentCommand command) {
- final CaseEntity customerCase = getCaseOrThrow(command.getProductIdentifier(), command.getCaseIdentifier());
- checkActionCanBeExecuted(Case.State.valueOf(customerCase.getCurrentState()), Action.ACCEPT_PAYMENT);
- updateCaseState(customerCase, Case.State.ACTIVE);
+ final String productIdentifier = command.getProductIdentifier();
+ final String caseIdentifier = command.getCaseIdentifier();
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.ACCEPT_PAYMENT);
+ updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.ACTIVE);
return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
}
@@ -180,9 +185,11 @@
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.WRITE_OFF_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final WriteOffCommand command) {
- final CaseEntity customerCase = getCaseOrThrow(command.getProductIdentifier(), command.getCaseIdentifier());
- checkActionCanBeExecuted(Case.State.valueOf(customerCase.getCurrentState()), Action.WRITE_OFF);
- updateCaseState(customerCase, Case.State.CLOSED);
+ final String productIdentifier = command.getProductIdentifier();
+ final String caseIdentifier = command.getCaseIdentifier();
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.WRITE_OFF);
+ updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.CLOSED);
return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
}
@@ -190,9 +197,11 @@
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.CLOSE_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final CloseCommand command) {
- final CaseEntity customerCase = getCaseOrThrow(command.getProductIdentifier(), command.getCaseIdentifier());
- checkActionCanBeExecuted(Case.State.valueOf(customerCase.getCurrentState()), Action.CLOSE);
- updateCaseState(customerCase, Case.State.CLOSED);
+ final String productIdentifier = command.getProductIdentifier();
+ final String caseIdentifier = command.getCaseIdentifier();
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.CLOSE);
+ updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.CLOSED);
return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
}
@@ -200,20 +209,16 @@
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
@EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.RECOVER_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final RecoverCommand command) {
- final CaseEntity customerCase = getCaseOrThrow(command.getProductIdentifier(), command.getCaseIdentifier());
- checkActionCanBeExecuted(Case.State.valueOf(customerCase.getCurrentState()), Action.RECOVER);
- updateCaseState(customerCase, Case.State.CLOSED);
+ final String productIdentifier = command.getProductIdentifier();
+ final String caseIdentifier = command.getCaseIdentifier();
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.RECOVER);
+ updateCaseState(dataContextOfAction.getCustomerCase(), Case.State.CLOSED);
return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier(), "x");
}
- private CaseEntity getCaseOrThrow(final String productIdentifier, final String caseIdentifier) {
- return caseRepository.findByProductIdentifierAndIdentifier(productIdentifier, caseIdentifier)
- .orElseThrow(() -> ServiceException.notFound("Case not found ''{0}.{1}''.", productIdentifier, caseIdentifier));
- }
-
- private ProductEntity getProductOrThrow(final String productIdentifier) {
- return productRepository.findByIdentifier(productIdentifier)
- .orElseThrow(() -> ServiceException.notFound("Product not found ''{0}''.", productIdentifier));
+ private static LocalDate today() {
+ return LocalDate.now(ZoneId.of("UTC"));
}
private void checkActionCanBeExecuted(final Case.State state, final Action action) {
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
new file mode 100644
index 0000000..be997ee
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
+import io.mifos.individuallending.internal.repository.CaseParametersRepository;
+import io.mifos.portfolio.api.v1.domain.AccountAssignment;
+import io.mifos.portfolio.api.v1.domain.Case;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
+import io.mifos.portfolio.service.internal.repository.CaseEntity;
+import io.mifos.portfolio.service.internal.repository.CaseRepository;
+import io.mifos.portfolio.service.internal.repository.ProductEntity;
+import io.mifos.portfolio.service.internal.repository.ProductRepository;
+import io.mifos.portfolio.service.internal.util.AccountingAdapter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class CostComponentService {
+
+ private final ProductRepository productRepository;
+ private final CaseRepository caseRepository;
+ private final CaseParametersRepository caseParametersRepository;
+ private final IndividualLoanService individualLoanService;
+ private final AccountingAdapter accountingAdapter;
+
+ @Autowired
+ public CostComponentService(
+ final ProductRepository productRepository,
+ final CaseRepository caseRepository,
+ final CaseParametersRepository caseParametersRepository,
+ final IndividualLoanService individualLoanService,
+ final AccountingAdapter accountingAdapter) {
+ this.productRepository = productRepository;
+ this.caseRepository = caseRepository;
+ this.caseParametersRepository = caseParametersRepository;
+ this.individualLoanService = individualLoanService;
+ this.accountingAdapter = accountingAdapter;
+ }
+
+ public DataContextOfAction checkedGetDataContext(
+ final String productIdentifier,
+ final String caseIdentifier,
+ final List<AccountAssignment> oneTimeAccountAssignments) {
+
+ final ProductEntity product =
+ productRepository.findByIdentifier(productIdentifier)
+ .orElseThrow(() -> ServiceException.notFound("Product not found ''{0}''.", productIdentifier));
+ final CaseEntity customerCase =
+ caseRepository.findByProductIdentifierAndIdentifier(productIdentifier, caseIdentifier)
+ .orElseThrow(() -> ServiceException.notFound("Case not found ''{0}.{1}''.", productIdentifier, caseIdentifier));
+
+ final CaseParameters caseParameters =
+ caseParametersRepository.findByCaseId(customerCase.getId())
+ .map(CaseParametersMapper::mapEntity)
+ .orElseThrow(() -> ServiceException.notFound(
+ "Individual loan not found ''{0}.{1}''.",
+ productIdentifier, caseIdentifier));
+
+ return new DataContextOfAction(product, customerCase, caseParameters, oneTimeAccountAssignments);
+ }
+
+ public List<CostComponent> getCostComponents(final String productIdentifier, final String caseIdentifier, final Action action) {
+ final DataContextOfAction context = checkedGetDataContext(productIdentifier, caseIdentifier, Collections.emptyList());
+ final Case.State caseState = Case.State.valueOf(context.getCustomerCase().getCurrentState());
+ final BigDecimal runningBalance;
+ if (caseState == Case.State.ACTIVE) {
+ final DesignatorToAccountIdentifierMapper mapper = new DesignatorToAccountIdentifierMapper(context);
+ final String customerLoanAccountIdentifier = mapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
+ runningBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+ }
+ else
+ runningBalance = BigDecimal.ZERO;
+
+ return individualLoanService.getCostComponentsForRepaymentPeriod(productIdentifier, context.getCaseParameters(), runningBalance, action, LocalDate.now(ZoneId.of("UTC")), LocalDate.now(ZoneId.of("UTC")))
+ .stream()
+ .map(x -> new CostComponent(x.getKey().getIdentifier(), x.getValue().getAmount()))
+ .collect(Collectors.toList()); //TODO: initial disbursal date.
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
new file mode 100644
index 0000000..a51e975
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
+
+import java.math.BigDecimal;
+import java.util.Map;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+public class CostComponentsForRepaymentPeriod {
+ final Map<ChargeDefinition, CostComponent> costComponents;
+ final BigDecimal balanceAdjustment;
+
+ CostComponentsForRepaymentPeriod(
+ final Map<ChargeDefinition, CostComponent> costComponents,
+ final BigDecimal balanceAdjustment) {
+ this.costComponents = costComponents;
+ this.balanceAdjustment = balanceAdjustment;
+ }
+
+ public Stream<Map.Entry<ChargeDefinition, CostComponent>> stream() {
+ return costComponents.entrySet().stream();
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
new file mode 100644
index 0000000..7687b3c
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
+import io.mifos.portfolio.api.v1.domain.AccountAssignment;
+import io.mifos.portfolio.service.internal.repository.CaseEntity;
+import io.mifos.portfolio.service.internal.repository.ProductEntity;
+
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+public class DataContextOfAction {
+ private final ProductEntity product;
+ private final CaseEntity customerCase;
+ private final CaseParameters caseParameters;
+ private final List<AccountAssignment> oneTimeAccountAssignments;
+
+ DataContextOfAction(final ProductEntity product,
+ final CaseEntity customerCase,
+ final CaseParameters caseParameters,
+ final List<AccountAssignment> oneTimeAccountAssignments) {
+ this.product = product;
+ this.customerCase = customerCase;
+ this.caseParameters = caseParameters;
+ this.oneTimeAccountAssignments = oneTimeAccountAssignments;
+ }
+
+ public ProductEntity getProduct() {
+ return product;
+ }
+
+ public CaseEntity getCustomerCase() {
+ return customerCase;
+ }
+
+ public CaseParameters getCaseParameters() {
+ return caseParameters;
+ }
+
+ public List<AccountAssignment> getOneTimeAccountAssignments() {
+ return oneTimeAccountAssignments;
+ }
+}
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
new file mode 100644
index 0000000..e082f9b
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.core.lang.ServiceException;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.portfolio.api.v1.domain.AccountAssignment;
+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 java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * @author Myrle Krantz
+ */
+public class DesignatorToAccountIdentifierMapper {
+ private final Set<ProductAccountAssignmentEntity> productAccountAssignments;
+ private final Set<CaseAccountAssignmentEntity> caseAccountAssignments;
+ private final List<AccountAssignment> oneTimeAccountAssignments;
+
+ public DesignatorToAccountIdentifierMapper(final DataContextOfAction dataContextOfAction) {
+ this.productAccountAssignments = dataContextOfAction.getProduct().getAccountAssignments();
+ this.caseAccountAssignments = dataContextOfAction.getCustomerCase().getAccountAssignments();
+ this.oneTimeAccountAssignments = dataContextOfAction.getOneTimeAccountAssignments();
+ }
+
+ private Stream<AccountAssignment> allAccountAssignmentsAsStream() {
+ return Stream.concat(oneTimeAccountAssignments.stream(), fixedAccountAssignmentsAsStream());
+ }
+
+ private Stream<AccountAssignment> fixedAccountAssignmentsAsStream() {
+ return Stream.concat(caseAccountAssignments.stream().map(CaseMapper::mapAccountAssignmentEntity),
+ productAccountAssignments.stream().map(ProductMapper::mapAccountAssignmentEntity));
+ }
+
+ public String mapOrThrow(final String accountDesignator) {
+ return allAccountAssignmentsAsStream()
+ .filter(x -> x.getDesignator().equals(accountDesignator))
+ .findFirst()
+ .map(AccountAssignment::getAccountIdentifier)
+ .orElseThrow(() -> ServiceException.badRequest("A required account designator was not set ''{0}''.", accountDesignator));
+ }
+
+ public Stream<AccountAssignment> getLedgersNeedingAccounts() {
+ return fixedAccountAssignmentsAsStream()
+ .filter(x -> !x.getDesignator().equals(AccountDesignators.ENTRY))
+ .filter(x -> (x.getAccountIdentifier() == null) && (x.getLedgerIdentifier() != null));
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java b/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
index e3ec87b..4167b81 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java
@@ -15,19 +15,19 @@
*/
package io.mifos.individuallending.internal.service;
-import io.mifos.portfolio.api.v1.domain.CostComponent;
-import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
-import io.mifos.portfolio.service.internal.service.ProductService;
import io.mifos.core.lang.DateConverter;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.caseinstance.ChargeName;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
import io.mifos.portfolio.api.v1.domain.Product;
-import io.mifos.portfolio.service.internal.util.ChargeInstance;
+import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
+import io.mifos.portfolio.service.internal.service.ProductService;
import org.javamoney.calc.common.Rate;
import org.javamoney.moneta.Money;
import org.springframework.beans.factory.annotation.Autowired;
@@ -38,13 +38,13 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
-import java.util.function.Function;
+import java.util.function.BiFunction;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.PENDING_DISBURSAL;
import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.CUSTOMER_LOAN;
+import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.PENDING_DISBURSAL;
import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.PAYMENT_ID;
import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.PAYMENT_NAME;
import static io.mifos.portfolio.api.v1.domain.ChargeDefinition.ChargeMethod.FIXED;
@@ -113,27 +113,23 @@
return ret;
}
- public List<ChargeInstance> getChargeInstances(final String productIdentifier,
- final CaseParameters caseParameters,
- final BigDecimal currentBalance,
- final Action action,
- final LocalDate initialDisbursalDate,
- final LocalDate forDate) {
+ public CostComponentsForRepaymentPeriod getCostComponentsForRepaymentPeriod(final String productIdentifier,
+ final CaseParameters caseParameters,
+ final BigDecimal runningBalance,
+ final Action action,
+ final LocalDate initialDisbursalDate,
+ final LocalDate forDate) {
final Product product = productService.findByIdentifier(productIdentifier)
.orElseThrow(() -> new IllegalArgumentException("Non-existent product identifier."));
final int minorCurrencyUnitDigits = product.getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = scheduledActionService.getScheduledActions(initialDisbursalDate, caseParameters, action, forDate);
- final List<ScheduledCharge> scheduledCharges = getScheduledCharges(productIdentifier, minorCurrencyUnitDigits, currentBalance, scheduledActions);
+ final List<ScheduledCharge> scheduledCharges = getScheduledCharges(productIdentifier, minorCurrencyUnitDigits, runningBalance, scheduledActions);
- final CostComponentsForRepaymentPeriod costComponentsForScheduledCharges = getCostComponentsForScheduledCharges(scheduledCharges, currentBalance, minorCurrencyUnitDigits);
-
- return costComponentsForScheduledCharges.costComponents.entrySet().stream()
- .map(IndividualLoanService::mapToChargeInstance)
- .collect(Collectors.toList());
- }
-
- private static ChargeInstance mapToChargeInstance(final Map.Entry<ChargeDefinition, CostComponent> x) {
- return new ChargeInstance(x.getKey().getFromAccountDesignator(), x.getKey().getToAccountDesignator(), x.getValue().getAmount());
+ return getCostComponentsForScheduledCharges(
+ scheduledCharges,
+ caseParameters.getMaximumBalance(),
+ runningBalance,
+ minorCurrencyUnitDigits);
}
private static ChargeName chargeNameFromChargeDefinition(final ScheduledCharge scheduledCharge) {
@@ -221,7 +217,7 @@
{
final SortedSet<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- getCostComponentsForScheduledCharges(scheduledChargesInPeriod, balance, minorCurrencyUnitDigits);
+ getCostComponentsForScheduledCharges(scheduledChargesInPeriod, balance, balance, minorCurrencyUnitDigits);
final PlannedPayment plannedPayment = new PlannedPayment();
plannedPayment.setCostComponents(costComponentsForRepaymentPeriod.costComponents.values().stream().collect(Collectors.toList()));
@@ -243,21 +239,10 @@
return plannedPayments;
}
- private static class CostComponentsForRepaymentPeriod {
- final Map<ChargeDefinition, CostComponent> costComponents;
- final BigDecimal balanceAdjustment;
-
- private CostComponentsForRepaymentPeriod(
- final Map<ChargeDefinition, CostComponent> costComponents,
- final BigDecimal balanceAdjustment) {
- this.costComponents = costComponents;
- this.balanceAdjustment = balanceAdjustment;
- }
- }
-
static private CostComponentsForRepaymentPeriod getCostComponentsForScheduledCharges(
final Collection<ScheduledCharge> scheduledCharges,
- final BigDecimal balance,
+ final BigDecimal maximumBalance,
+ final BigDecimal runningBalance,
final int minorCurrencyUnitDigits) {
BigDecimal balanceAdjustment = BigDecimal.ZERO;
@@ -274,7 +259,7 @@
});
final BigDecimal chargeAmount = howToApplyScheduledChargeToBalance(scheduledCharge, 8)
- .apply(balance)
+ .apply(maximumBalance, runningBalance)
.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
if (chargeDefinitionTouchesCustomerLoanAccount(scheduledCharge.getChargeDefinition()))
balanceAdjustment = balanceAdjustment.add(chargeAmount);
@@ -293,18 +278,25 @@
(chargeDefinition.getAccrualAccountDesignator() != null && chargeDefinition.getAccrualAccountDesignator().equals(AccountDesignators.CUSTOMER_LOAN));
}
- private static Function<BigDecimal, BigDecimal> howToApplyScheduledChargeToBalance(
+ private static BiFunction<BigDecimal, BigDecimal, BigDecimal> howToApplyScheduledChargeToBalance(
final ScheduledCharge scheduledCharge,
final int precision)
{
+
switch (scheduledCharge.getChargeDefinition().getChargeMethod())
{
case FIXED:
- return (x) -> scheduledCharge.getChargeDefinition().getAmount();
- case PROPORTIONAL:
- return (x) -> PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, precision).multiply(x);
+ return (maximumBalance, runningBalance) -> scheduledCharge.getChargeDefinition().getAmount();
+ case PROPORTIONAL: {
+ if (scheduledCharge.getChargeDefinition().getProportionalTo().equals(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR))
+ return (maximumBalance, runningBalance) -> PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, precision).multiply(runningBalance);
+ else if (scheduledCharge.getChargeDefinition().getProportionalTo().equals(ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR))
+ return (maximumBalance, runningBalance) -> PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, precision).multiply(maximumBalance);
+ else //TODO: correctly implement charges which are proportionate to other charges.
+ return (maximumBalance, runningBalance) -> PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, precision).multiply(maximumBalance);
+ }
default:
- return (x) -> BigDecimal.ZERO;
+ return (maximumBalance, runningBalance) -> 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 1a48fec..f4bff53 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
@@ -19,6 +19,8 @@
import io.mifos.portfolio.service.internal.repository.ChargeDefinitionEntity;
import io.mifos.portfolio.service.internal.repository.ProductEntity;
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
+
/**
* @author Myrle Krantz
*/
@@ -35,6 +37,7 @@
ret.setChargeAction(chargeDefinition.getChargeAction());
ret.setAmount(chargeDefinition.getAmount());
ret.setChargeMethod(chargeDefinition.getChargeMethod());
+ ret.setProportionalTo(chargeDefinition.getProportionalTo());
ret.setForCycleSizeUnit(chargeDefinition.getForCycleSizeUnit());
ret.setFromAccountDesignator(chargeDefinition.getFromAccountDesignator());
ret.setAccrualAccountDesignator(chargeDefinition.getAccrualAccountDesignator());
@@ -53,6 +56,7 @@
ret.setChargeAction(from.getChargeAction());
ret.setAmount(from.getAmount());
ret.setChargeMethod(from.getChargeMethod());
+ ret.setProportionalTo(proportionalToLegacyMapper(from, from.getChargeMethod(), from.getIdentifier()));
ret.setForCycleSizeUnit(from.getForCycleSizeUnit());
ret.setFromAccountDesignator(from.getFromAccountDesignator());
ret.setAccrualAccountDesignator(from.getAccrualAccountDesignator());
@@ -60,4 +64,22 @@
return ret;
}
+
+ private static String proportionalToLegacyMapper(final ChargeDefinitionEntity from,
+ final ChargeDefinition.ChargeMethod chargeMethod,
+ final String identifier) {
+ if ((chargeMethod == ChargeDefinition.ChargeMethod.FIXED) || (from.getProportionalTo() != null))
+ return from.getProportionalTo();
+
+ if (identifier.equals(LOAN_FUNDS_ALLOCATION_ID))
+ return MAXIMUM_BALANCE_DESIGNATOR;
+ else if (identifier.equals(LOAN_ORIGINATION_FEE_ID))
+ return MAXIMUM_BALANCE_DESIGNATOR;
+ else if (identifier.equals(PROCESSING_FEE_ID))
+ return MAXIMUM_BALANCE_DESIGNATOR;
+ else if (identifier.equals(LATE_FEE_ID))
+ return PAYMENT_ID;
+ else
+ return RUNNING_BALANCE_DESIGNATOR;
+ }
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java
index 92c436c..3f0eab0 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/ChargeDefinitionEntity.java
@@ -20,6 +20,7 @@
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.temporal.ChronoUnit;
+import java.util.Objects;
/**
* @author Myrle Krantz
@@ -59,6 +60,9 @@
@Column(name = "charge_method")
private ChargeDefinition.ChargeMethod chargeMethod;
+ @Column(name = "proportional_to")
+ private String proportionalTo;
+
@Column(name = "from_account_designator")
private String fromAccountDesignator;
@@ -147,6 +151,14 @@
this.chargeMethod = chargeMethod;
}
+ public String getProportionalTo() {
+ return proportionalTo;
+ }
+
+ public void setProportionalTo(String proportionalTo) {
+ this.proportionalTo = proportionalTo;
+ }
+
public String getFromAccountDesignator() {
return fromAccountDesignator;
}
@@ -178,4 +190,18 @@
public void setForCycleSizeUnit(ChronoUnit forCycleSizeUnit) {
this.forCycleSizeUnit = forCycleSizeUnit;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ChargeDefinitionEntity that = (ChargeDefinitionEntity) o;
+ return Objects.equals(identifier, that.identifier) &&
+ Objects.equals(product, that.product);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(identifier, product);
+ }
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
index 11368c1..a6b96e4 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java
@@ -18,7 +18,6 @@
import io.mifos.core.lang.ServiceException;
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.CasePage;
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.CostComponent;
import io.mifos.portfolio.service.internal.mapper.CaseMapper;
import io.mifos.portfolio.service.internal.pattern.PatternFactoryRegistry;
@@ -35,7 +34,10 @@
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
-import java.util.*;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -47,18 +49,15 @@
private final PatternFactoryRegistry patternFactoryRegistry;
private final ProductRepository productRepository;
private final CaseRepository caseRepository;
- private final ChargeDefinitionService chargeDefinitionService;
@Autowired
public CaseService(
final PatternFactoryRegistry patternFactoryRegistry,
final ProductRepository productRepository,
- final CaseRepository caseRepository,
- final ChargeDefinitionService chargeDefinitionService) {
+ final CaseRepository caseRepository) {
this.patternFactoryRegistry = patternFactoryRegistry;
this.productRepository = productRepository;
this.caseRepository = caseRepository;
- this.chargeDefinitionService = chargeDefinitionService;
}
public CasePage findAllEntities(final String productIdentifier,
@@ -127,14 +126,7 @@
public List<CostComponent> getActionCostComponentsForCase(final String productIdentifier,
final String caseIdentifier,
final String actionIdentifier) {
- final Map<String, List<ChargeDefinition>> chargeDefinitions = chargeDefinitionService.getChargeDefinitionsMappedByChargeAction(productIdentifier);
- final List<ChargeDefinition> chargeDefinitionsForAction = chargeDefinitions.get(actionIdentifier);
- return chargeDefinitionsForAction.stream().map(x -> {
- final CostComponent ret = new CostComponent();
- ret.setChargeIdentifier(x.getIdentifier());
- ret.setAmount(x.getAmount()); //TODO: This is too simplistic. Will only work for fixed charges and no accrual.
- return ret;
- }).collect(Collectors.toList());
-
+ return getPatternFactoryOrThrow(productIdentifier)
+ .getCostComponentsForAction(productIdentifier, caseIdentifier, actionIdentifier);
}
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
index f802c51..f88a277 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
@@ -18,10 +18,7 @@
import io.mifos.accounting.api.v1.client.AccountNotFoundException;
import io.mifos.accounting.api.v1.client.LedgerManager;
import io.mifos.accounting.api.v1.client.LedgerNotFoundException;
-import io.mifos.accounting.api.v1.domain.Account;
-import io.mifos.accounting.api.v1.domain.Creditor;
-import io.mifos.accounting.api.v1.domain.Debtor;
-import io.mifos.accounting.api.v1.domain.JournalEntry;
+import io.mifos.accounting.api.v1.domain.*;
import io.mifos.core.api.util.UserContextHolder;
import io.mifos.core.lang.DateConverter;
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
@@ -109,6 +106,24 @@
return BigDecimal.valueOf(account.getBalance());
}
+ public String createAccountForLedgerAssignment(final String customerIdentifier, final AccountAssignment ledgerAssignment) {
+ final Ledger ledger = ledgerManager.findLedger(ledgerAssignment.getLedgerIdentifier());
+ final AccountPage accountsOfLedger = ledgerManager.fetchAccountsOfLedger(ledger.getIdentifier(), null, null, null, null);
+
+ final Account generatedAccount = new Account();
+ generatedAccount.setBalance(0.0);
+ generatedAccount.setType(ledger.getType());
+ generatedAccount.setState(Account.State.OPEN.name());
+ final String accountNumber = customerIdentifier + "." + ledgerAssignment.getDesignator()
+ + "." + String.format("%05d", accountsOfLedger.getTotalElements() + 1);
+ generatedAccount.setIdentifier(accountNumber);
+ generatedAccount.setLedger(ledger.getIdentifier());
+ generatedAccount.setName(accountNumber);
+ ledgerManager.createAccount(generatedAccount);
+
+ return accountNumber;
+ }
+
public static boolean accountAssignmentsCoverChargeDefinitions(
final Set<AccountAssignment> accountAssignments,
diff --git a/service/src/main/java/io/mifos/products/spi/PatternFactory.java b/service/src/main/java/io/mifos/products/spi/PatternFactory.java
index 36bbb81..74a0f16 100644
--- a/service/src/main/java/io/mifos/products/spi/PatternFactory.java
+++ b/service/src/main/java/io/mifos/products/spi/PatternFactory.java
@@ -18,6 +18,7 @@
import io.mifos.portfolio.api.v1.domain.Case;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
import io.mifos.portfolio.api.v1.domain.Pattern;
import java.util.List;
@@ -33,6 +34,7 @@
void persistParameters(Long caseId, String parameters);
void changeParameters(Long caseId, String parameters);
Optional<String> getParameters(Long caseId);
- Set<String> getNextActionsForState(final Case.State state);
+ Set<String> getNextActionsForState(Case.State state);
+ List<CostComponent> getCostComponentsForAction(String productIdentifier, String caseIdentifier, String actionIdentifier);
ProductCommandDispatcher getIndividualLendingCommandDispatcher();
}
diff --git a/service/src/main/resources/db/migrations/mariadb/V2__in_motion.sql b/service/src/main/resources/db/migrations/mariadb/V2__in_motion.sql
new file mode 100644
index 0000000..f094533
--- /dev/null
+++ b/service/src/main/resources/db/migrations/mariadb/V2__in_motion.sql
@@ -0,0 +1,19 @@
+--
+-- Copyright 2017 The Mifos Initiative.
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+# noinspection SqlNoDataSourceInspectionForFile
+
+ALTER TABLE bastet_p_chrg_defs ADD COLUMN proportional_to VARCHAR(32) NULL;
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java
index 08f2c5c..70b5b34 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
@@ -40,7 +40,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.PROCESSING_FEE_ID;
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
/**
* @author Myrle Krantz
@@ -98,6 +98,9 @@
private Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction;
private Set<String> expectedChargeIdentifiers = new HashSet<>(Arrays.asList(ChargeIdentifiers.INTEREST_ID, ChargeIdentifiers.PAYMENT_ID));
private Map<ActionDatePair, List<ChargeInstance>> chargeInstancesForActions = new HashMap<>();
+ //This is an abuse of the ChargeInstance since everywhere else it's intended to contain account identifiers and not
+ //account designators. Don't copy the code around charge instances in this test without thinking about what you're
+ //doing carefully first.
TestCase(final String description) {
this.description = description;
@@ -175,7 +178,12 @@
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = new HashMap<>();
chargeDefinitionsMappedByAction.put(Action.APPLY_INTEREST.name(), getInterestChargeDefinition(0.01, ChronoUnit.YEARS));
chargeDefinitionsMappedByAction.put(Action.OPEN.name(),
- getFixedSingleChargeDefinition(10.0, Action.OPEN, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME));
+ Collections.singletonList(
+ getFixedSingleChargeDefinition(10.0, Action.OPEN, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME)));
+ chargeDefinitionsMappedByAction.put(Action.APPROVE.name(),
+ Arrays.asList(
+ getFixedSingleChargeDefinition(100.0, Action.APPROVE, LOAN_ORIGINATION_FEE_ID, AccountDesignators.ORIGINATION_FEE_INCOME),
+ getProportionalSingleChargeDefinition(1.0, Action.APPROVE, LOAN_FUNDS_ALLOCATION_ID, AccountDesignators.LOAN_FUNDS_SOURCE, AccountDesignators.PENDING_DISBURSAL)));
return new TestCase("simpleCase")
.minorCurrencyUnitDigits(2)
@@ -183,11 +191,24 @@
.initialDisbursementDate(initialDisbursementDate)
.chargeDefinitionsMappedByAction(chargeDefinitionsMappedByAction)
.expectAdditionalChargeIdentifier(PROCESSING_FEE_ID)
+ .expectAdditionalChargeIdentifier(LOAN_FUNDS_ALLOCATION_ID)
+ .expectAdditionalChargeIdentifier(LOAN_ORIGINATION_FEE_ID)
.expectChargeInstancesForActionDatePair(Action.OPEN, initialDisbursementDate,
Collections.singletonList(new ChargeInstance(
AccountDesignators.ENTRY,
AccountDesignators.PROCESSING_FEE_INCOME,
- BigDecimal.valueOf(10).setScale(2, BigDecimal.ROUND_UNNECESSARY))));
+ BigDecimal.valueOf(10).setScale(2, BigDecimal.ROUND_UNNECESSARY))))
+ .expectChargeInstancesForActionDatePair(Action.APPROVE, initialDisbursementDate,
+ Arrays.asList(
+ new ChargeInstance(
+ AccountDesignators.ENTRY,
+ AccountDesignators.ORIGINATION_FEE_INCOME,
+ BigDecimal.valueOf(100.0).setScale(2, BigDecimal.ROUND_UNNECESSARY)),
+ new ChargeInstance(
+ AccountDesignators.LOAN_FUNDS_SOURCE,
+ AccountDesignators.PENDING_DISBURSAL,
+ caseParameters.getMaximumBalance().setScale(2, BigDecimal.ROUND_UNNECESSARY)
+ )));
}
private static TestCase yearLoanTestCase()
@@ -217,18 +238,20 @@
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, 1, 0, 0));
caseParameters.setMaximumBalance(BigDecimal.valueOf(2000));
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = new HashMap<>();
- chargeDefinitionsMappedByAction.put(Action.APPLY_INTEREST.name(), getInterestChargeDefinition(0.05, ChronoUnit.YEARS));
-
final List<ChargeDefinition> defaultLoanCharges = IndividualLendingPatternFactory.defaultIndividualLoanCharges();
- defaultLoanCharges.forEach(x -> chargeDefinitionsMappedByAction.put(x.getChargeAction(), Collections.singletonList(x)));
+
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = defaultLoanCharges.stream()
+ .collect(Collectors.groupingBy(ChargeDefinition::getChargeAction,
+ Collectors.mapping(x -> x, Collectors.toList())));
+
+ chargeDefinitionsMappedByAction.put(Action.APPLY_INTEREST.name(), getInterestChargeDefinition(0.05, ChronoUnit.YEARS));
return new TestCase("chargeDefaultsCase")
.minorCurrencyUnitDigits(2)
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.chargeDefinitionsMappedByAction(chargeDefinitionsMappedByAction)
- .expectedChargeIdentifiers(new HashSet<>(Arrays.asList(PROCESSING_FEE_ID, ChargeIdentifiers.RETURN_DISBURSEMENT_ID, ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, ChargeIdentifiers.INTEREST_ID, ChargeIdentifiers.PAYMENT_ID)));
+ .expectedChargeIdentifiers(new HashSet<>(Arrays.asList(PROCESSING_FEE_ID, LOAN_FUNDS_ALLOCATION_ID, RETURN_DISBURSEMENT_ID, LOAN_ORIGINATION_FEE_ID, INTEREST_ID, PAYMENT_ID)));
}
private static List<ChargeDefinition> getInterestChargeDefinition(final double amount, final ChronoUnit forCycleSizeUnit) {
@@ -238,6 +261,7 @@
ret.setAccrueAction(Action.APPLY_INTEREST.name());
ret.setChargeAction(Action.ACCEPT_PAYMENT.name());
ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ ret.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR);
ret.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN);
ret.setAccrualAccountDesignator(AccountDesignators.INTEREST_ACCRUAL);
ret.setToAccountDesignator(AccountDesignators.INTEREST_INCOME);
@@ -245,7 +269,7 @@
return Collections.singletonList(ret);
}
- private static List<ChargeDefinition> getFixedSingleChargeDefinition(
+ private static ChargeDefinition getFixedSingleChargeDefinition(
final double amount,
final Action action,
final String chargeIdentifier,
@@ -256,10 +280,30 @@
ret.setAccrueAction(null);
ret.setChargeAction(action.name());
ret.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
+ ret.setProportionalTo(null);
ret.setFromAccountDesignator(AccountDesignators.ENTRY);
ret.setToAccountDesignator(feeAccountDesignator);
ret.setForCycleSizeUnit(null);
- return Collections.singletonList(ret);
+ return ret;
+ }
+
+ private static ChargeDefinition getProportionalSingleChargeDefinition(
+ final double amount,
+ final Action action,
+ final String chargeIdentifier,
+ final String fromAccountDesignator,
+ final String toAccountDesignator) {
+ final ChargeDefinition ret = new ChargeDefinition();
+ ret.setAmount(BigDecimal.valueOf(amount));
+ ret.setIdentifier(chargeIdentifier);
+ ret.setAccrueAction(null);
+ ret.setChargeAction(action.name());
+ ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ ret.setProportionalTo(ChargeIdentifiers.MAXIMUM_BALANCE_DESIGNATOR);
+ ret.setFromAccountDesignator(fromAccountDesignator);
+ ret.setToAccountDesignator(toAccountDesignator);
+ ret.setForCycleSizeUnit(null);
+ return ret;
}
public IndividualLoanServiceTest(final TestCase testCase)
@@ -334,16 +378,20 @@
}
@Test
- public void createChargeInstances() {
+ public void getCostComponentsForRepaymentPeriod() {
testCase.chargeInstancesForActions.entrySet().forEach(entry ->
Assert.assertEquals(
- entry.getValue(),
- testSubject.getChargeInstances(
+ entry.getValue().stream().collect(Collectors.toSet()),
+ testSubject.getCostComponentsForRepaymentPeriod(
testCase.productIdentifier,
testCase.caseParameters,
testCase.caseParameters.getMaximumBalance(),
entry.getKey().getAction(),
- testCase.initialDisbursementDate, entry.getKey().getLocalDate())));
+ testCase.initialDisbursementDate, entry.getKey().getLocalDate())
+ .stream()
+ .map(x -> new ChargeInstance(x.getKey().getFromAccountDesignator(), x.getKey().getToAccountDesignator(), x.getValue().getAmount()))
+ .collect(Collectors.toSet())
+ ));
}
private double percentDifference(final BigDecimal maxPayment, final BigDecimal minPayment) {