Intermediate state so that Mark can start programming against the new account designator.
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 dc65e9b..65bd4e6 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
@@ -23,6 +23,7 @@
String CUSTOMER_LOAN = "customer-loan";
String PENDING_DISBURSAL = "pending-disbursal";
String LOAN_FUNDS_SOURCE = "loan-funds-source";
+ String LOANS_PAYABLE = "loans-payable";
String PROCESSING_FEE_INCOME = "processing-fee-income";
String ORIGINATION_FEE_INCOME = "origination-fee-income";
String DISBURSEMENT_FEE_INCOME = "disbursement-fee-income";
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 37b337a..65e8117 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
@@ -34,15 +34,21 @@
String LATE_FEE_ID = nameToIdentifier(LATE_FEE_NAME);
String DISBURSEMENT_FEE_NAME = "Disbursement fee";
String DISBURSEMENT_FEE_ID = nameToIdentifier(DISBURSEMENT_FEE_NAME);
+ String DISBURSE_PAYMENT_NAME = "Disburse payment";
+ String DISBURSE_PAYMENT_ID = nameToIdentifier(DISBURSE_PAYMENT_NAME);
+ String TRACK_DISBURSAL_PAYMENT_NAME = "Track disburse payment";
+ String TRACK_DISBURSAL_PAYMENT_ID = nameToIdentifier(TRACK_DISBURSAL_PAYMENT_NAME);
String LOAN_ORIGINATION_FEE_NAME = "Loan origination fee";
String LOAN_ORIGINATION_FEE_ID = nameToIdentifier(LOAN_ORIGINATION_FEE_NAME);
String PROCESSING_FEE_NAME = "Processing fee";
String PROCESSING_FEE_ID = nameToIdentifier(PROCESSING_FEE_NAME);
- String PAYMENT_NAME = "Payment";
- String PAYMENT_ID = nameToIdentifier(PAYMENT_NAME);
-
+ String REPAYMENT_NAME = "Repayment";
+ String REPAYMENT_ID = nameToIdentifier(REPAYMENT_NAME);
+ String TRACK_RETURN_PRINCIPAL_NAME = "Return principal";
+ String TRACK_RETURN_PRINCIPAL_ID = nameToIdentifier(TRACK_RETURN_PRINCIPAL_NAME);
String MAXIMUM_BALANCE_DESIGNATOR = "{maximumbalance}";
String RUNNING_BALANCE_DESIGNATOR = "{runningbalance}";
+ String PRINCIPAL_ADJUSTMENT_DESIGNATOR = "{principaladjustment}";
static String nameToIdentifier(String name) {
return name.toLowerCase(Locale.US).replace(" ", "-");
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
index 3fbacdd..1b2f61e 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
@@ -272,10 +272,21 @@
@PathVariable("caseidentifier") final String caseIdentifier);
@RequestMapping(
- value = "/products/{productidentifier}/cases/{caseidentifier}/actions/{actionidentifier}/costcomponents",
- method = RequestMethod.GET,
- produces = MediaType.ALL_VALUE,
- consumes = MediaType.APPLICATION_JSON_VALUE
+ value = "/products/{productidentifier}/cases/{caseidentifier}/actions/{actionidentifier}/costcomponents",
+ method = RequestMethod.GET,
+ produces = MediaType.ALL_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE
+ )
+ List<CostComponent> getCostComponentsForAction(@PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("caseidentifier") final String caseIdentifier,
+ @PathVariable("actionidentifier") final String actionIdentifier,
+ @RequestParam(value="touchingaccounts", required = false, defaultValue = "") final String forAccountDesignators);
+
+ @RequestMapping(
+ value = "/products/{productidentifier}/cases/{caseidentifier}/actions/{actionidentifier}/costcomponents",
+ method = RequestMethod.GET,
+ produces = MediaType.ALL_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE
)
List<CostComponent> getCostComponentsForAction(@PathVariable("productidentifier") final String productIdentifier,
@PathVariable("caseidentifier") final String caseIdentifier,
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 961d20d..593a999 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -56,8 +56,9 @@
static final String PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER = "1312";
static final String DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER = "1313";
static final String TELLER_ONE_ACCOUNT_IDENTIFIER = "7352";
- static final String LOAN_INTEREST_ACCRUAL_ACCOUNT = "7810";
- static final String CONSUMER_LOAN_INTEREST_ACCOUNT = "1103";
+ static final String LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER = "7810";
+ static final String CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER = "1103";
+ static final String LOANS_PAYABLE_ACCOUNT_IDENTIFIER ="missingInChartOfAccounts";
static final Map<String, AccountData> accountMap = new HashMap<>();
@@ -73,9 +74,10 @@
this.account.setBalance(balance);
}
- void addAccountEntry(final double amount) {
+ void addAccountEntry(final String message, final double amount) {
final AccountEntry accountEntry = new AccountEntry();
accountEntry.setAmount(amount);
+ accountEntry.setMessage(message);
accountEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now(Clock.systemUTC())));
accountEntries.add(accountEntry);
}
@@ -87,8 +89,7 @@
accountMap.put(account.getIdentifier(), accountData);
Mockito.doAnswer(new AccountEntriesStreamAnswer(accountData))
.when(ledgerManagerMock)
- .fetchAccountEntriesStream(Mockito.eq(account.getIdentifier()), Matchers.anyString(), Matchers.anyString());
-
+ .fetchAccountEntriesStream(Mockito.eq(account.getIdentifier()), Matchers.anyString(), Matchers.anyString(), Matchers.eq("ASC"));
}
@@ -198,7 +199,7 @@
private static Account loanInterestAccrualAccount() {
final Account ret = new Account();
- ret.setIdentifier(LOAN_INTEREST_ACCRUAL_ACCOUNT);
+ ret.setIdentifier(LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER);
ret.setLedger(ACCRUED_INCOME_LEDGER_IDENTIFIER);
ret.setType(AccountType.ASSET.name());
return ret;
@@ -206,12 +207,20 @@
private static Account consumerLoanInterestAccount() {
final Account ret = new Account();
- ret.setIdentifier(CONSUMER_LOAN_INTEREST_ACCOUNT);
+ ret.setIdentifier(CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER);
ret.setLedger(LOAN_INCOME_LEDGER_IDENTIFIER);
ret.setType(AccountType.REVENUE.name());
return ret;
}
+ private static Account loansPayableAccount() {
+ final Account ret = new Account();
+ ret.setIdentifier(LOANS_PAYABLE_ACCOUNT_IDENTIFIER);
+ //ret.setLedger(LOAN_INCOME_LEDGER_IDENTIFIER);
+ ret.setType(AccountType.LIABILITY.name());
+ return ret;
+ }
+
private static AccountPage customerLoanAccountsPage() {
final Account customerLoanAccount1 = new Account();
customerLoanAccount1.setIdentifier("customerLoanAccount1");
@@ -316,9 +325,6 @@
checkedArgument = (JournalEntry) argument;
- checkedArgument.getDebtors();
- checkedArgument.getCreditors();
-
return this.debtors.equals(checkedArgument.getDebtors()) &&
this.creditors.equals(checkedArgument.getCreditors());
}
@@ -337,6 +343,22 @@
}
}
+ private static class CreateJournalEntryAnswer implements Answer {
+ @Override
+ public Void answer(final InvocationOnMock invocation) throws Throwable {
+ final JournalEntry journalEntry = invocation.getArgumentAt(0, JournalEntry.class);
+ journalEntry.getCreditors().forEach(creditor ->
+ accountMap.get(creditor.getAccountNumber()).addAccountEntry(
+ journalEntry.getMessage(),
+ Double.valueOf(creditor.getAmount())));
+ journalEntry.getDebtors().forEach(debtor ->
+ accountMap.get(debtor.getAccountNumber()).addAccountEntry(
+ journalEntry.getMessage(),
+ Double.valueOf(debtor.getAmount())));
+ return null;
+ }
+ }
+
private static class FindAccountAnswer implements Answer {
@Override
public Account answer(final InvocationOnMock invocation) throws Throwable {
@@ -363,7 +385,11 @@
@Override
public Stream<AccountEntry> answer(final InvocationOnMock invocation) throws Throwable {
- return accountData.accountEntries.stream();
+ final String message = invocation.getArgumentAt(2, String.class);
+ if (message != null)
+ return accountData.accountEntries.stream().filter(x -> x.getMessage().equals(message));
+ else
+ return accountData.accountEntries.stream();
}
}
@@ -375,6 +401,7 @@
makeAccountResponsive(tellerOneAccount(), universalCreationDate, ledgerManagerMock);
makeAccountResponsive(loanInterestAccrualAccount(), universalCreationDate, ledgerManagerMock);
makeAccountResponsive(consumerLoanInterestAccount(), universalCreationDate, ledgerManagerMock);
+ makeAccountResponsive(loansPayableAccount(), universalCreationDate, ledgerManagerMock);
Mockito.doReturn(incomeLedger()).when(ledgerManagerMock).findLedger(INCOME_LEDGER_IDENTIFIER);
Mockito.doReturn(feesAndChargesLedger()).when(ledgerManagerMock).findLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
@@ -390,6 +417,7 @@
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));
}
static void mockBalance(final String accountIdentifier, final BigDecimal balance) {
@@ -412,8 +440,6 @@
Collections.singleton(new Debtor(fromAccountIdentifier, amount.toPlainString())),
Collections.singleton(new Creditor(toAccountIdentifier, amount.toPlainString())));
Mockito.verify(ledgerManager).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
- accountMap.get(fromAccountIdentifier).addAccountEntry(amount.doubleValue() * -1);
- accountMap.get(toAccountIdentifier).addAccountEntry(amount.doubleValue());
}
static void verifyTransfer(final LedgerManager ledgerManager,
@@ -421,8 +447,6 @@
final Set<Creditor> creditors) {
final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(debtors, creditors);
Mockito.verify(ledgerManager).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
- debtors.forEach(debtor -> accountMap.get(debtor.getAccountNumber()).addAccountEntry(Double.valueOf(debtor.getAmount())));
- creditors.forEach(creditor -> accountMap.get(creditor.getAccountNumber()).addAccountEntry(Double.valueOf(creditor.getAmount())));
}
}
\ 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 088bdbe..5910a20 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -16,11 +16,11 @@
package io.mifos.portfolio;
import com.google.gson.Gson;
+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.portfolio.api.v1.domain.*;
-import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.product.ProductParameters;
+import io.mifos.portfolio.api.v1.domain.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -66,8 +66,9 @@
accountAssignments.add(new AccountAssignment(PROCESSING_FEE_INCOME, PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(ORIGINATION_FEE_INCOME, LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(DISBURSEMENT_FEE_INCOME, DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER));
- accountAssignments.add(new AccountAssignment(INTEREST_INCOME, CONSUMER_LOAN_INTEREST_ACCOUNT));
- accountAssignments.add(new AccountAssignment(INTEREST_ACCRUAL, LOAN_INTEREST_ACCRUAL_ACCOUNT));
+ accountAssignments.add(new AccountAssignment(INTEREST_INCOME, CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER));
+ accountAssignments.add(new AccountAssignment(INTEREST_ACCRUAL, LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER));
+ accountAssignments.add(new AccountAssignment(LOANS_PAYABLE, LOANS_PAYABLE_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(LATE_FEE_INCOME, "001-008"));
accountAssignments.add(new AccountAssignment(LATE_FEE_ACCRUAL, "001-009"));
accountAssignments.add(new AccountAssignment(ARREARS_ALLOWANCE, "001-010"));
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 08bad1c..24f3441 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -63,6 +63,7 @@
private String customerLoanAccountIdentifier = null;
private BigDecimal expectedCurrentBalance = null;
+ private BigDecimal interestAccrued = BigDecimal.ZERO;
@Before
@@ -207,11 +208,13 @@
final Set<Debtor> debtors = new HashSet<>();
debtors.add(new Debtor(pendingDisbursalAccountIdentifier, caseParameters.getMaximumBalance().toPlainString()));
- debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, DISBURSEMENT_FEE_AMOUNT.toPlainString()));
+ debtors.add(new Debtor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER,
+ caseParameters.getMaximumBalance().subtract(DISBURSEMENT_FEE_AMOUNT).toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
creditors.add(new Creditor(customerLoanAccountIdentifier, caseParameters.getMaximumBalance().toPlainString()));
creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, DISBURSEMENT_FEE_AMOUNT.toPlainString()));
+ creditors.add(new Creditor(AccountingFixture.LOANS_PAYABLE_ACCOUNT_IDENTIFIER, caseParameters.getMaximumBalance().toPlainString()));
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
expectedCurrentBalance = expectedCurrentBalance.add(caseParameters.getMaximumBalance());
@@ -239,9 +242,11 @@
final BigDecimal calculatedInterest = caseParameters.getMaximumBalance().multiply(Fixture.INTEREST_RATE.divide(Fixture.ACCRUAL_PERIODS, 8, BigDecimal.ROUND_HALF_EVEN))
.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
+ interestAccrued = interestAccrued.add(calculatedInterest);
+
final Set<Debtor> debtors = new HashSet<>();
debtors.add(new Debtor(
- AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT,
+ AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER,
calculatedInterest.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
@@ -264,14 +269,20 @@
Action.ACCEPT_PAYMENT,
Collections.singletonList(assignEntryToTeller()),
IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
- Case.State.CLOSED);
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
+ Case.State.ACTIVE); //Close has to be done explicitly.
+ checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPLY_INTEREST,
+ Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
final Set<Debtor> debtors = new HashSet<>();
+ debtors.add(new Debtor(AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
+ debtors.add(new Debtor(AccountingFixture.LOANS_PAYABLE_ACCOUNT_IDENTIFIER, expectedCurrentBalance.subtract(interestAccrued).toPlainString()));
debtors.add(new Debtor(customerLoanAccountIdentifier, expectedCurrentBalance.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
+ creditors.add(new Creditor(AccountingFixture.CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
+ creditors.add(new Creditor(AccountingFixture.LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER, expectedCurrentBalance.subtract(interestAccrued).toPlainString()));
creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, expectedCurrentBalance.toPlainString()));
+
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
expectedCurrentBalance = expectedCurrentBalance.subtract(expectedCurrentBalance);
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 293abf2..59f3c27 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java
@@ -42,15 +42,20 @@
final List<ChargeDefinition> charges = portfolioManager.getAllChargeDefinitionsForProduct(product.getIdentifier());
final Set<String> chargeDefinitionIdentifiers = charges.stream().map(ChargeDefinition::getIdentifier).collect(Collectors.toSet());
final Set<String> expectedChargeDefinitionIdentifiers = Stream.of(
- ChargeIdentifiers.ALLOW_FOR_WRITE_OFF_ID,
- ChargeIdentifiers.DISBURSEMENT_FEE_ID,
- ChargeIdentifiers.INTEREST_ID,
- ChargeIdentifiers.LATE_FEE_ID,
- ChargeIdentifiers.LOAN_FUNDS_ALLOCATION_ID,
- ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID,
- ChargeIdentifiers.PROCESSING_FEE_ID,
- ChargeIdentifiers.RETURN_DISBURSEMENT_ID)
- .collect(Collectors.toSet());
+ ChargeIdentifiers.ALLOW_FOR_WRITE_OFF_ID,
+ ChargeIdentifiers.DISBURSEMENT_FEE_ID,
+ ChargeIdentifiers.INTEREST_ID,
+ ChargeIdentifiers.LATE_FEE_ID,
+ ChargeIdentifiers.LOAN_FUNDS_ALLOCATION_ID,
+ ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID,
+ ChargeIdentifiers.PROCESSING_FEE_ID,
+ ChargeIdentifiers.RETURN_DISBURSEMENT_ID,
+ ChargeIdentifiers.DISBURSE_PAYMENT_ID,
+ ChargeIdentifiers.TRACK_DISBURSAL_PAYMENT_ID,
+ ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID,
+ ChargeIdentifiers.REPAYMENT_ID
+ )
+ .collect(Collectors.toSet());
Assert.assertEquals(expectedChargeDefinitionIdentifiers, chargeDefinitionIdentifiers);
}
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
index c13daf4..e3520ad 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
@@ -18,19 +18,14 @@
import io.mifos.accounting.api.v1.domain.AccountEntry;
import io.mifos.core.lang.DateConverter;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
-import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
-import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.Case;
-import io.mifos.portfolio.api.v1.domain.Command;
import io.mifos.portfolio.api.v1.domain.Product;
-import org.junit.Assert;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mockito;
import java.time.LocalDateTime;
import java.util.Collections;
-import java.util.List;
import java.util.stream.Stream;
import static io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants.*;
@@ -81,7 +76,7 @@
firstEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now()));
Mockito.doAnswer((x) -> Stream.of(firstEntry))
.when(ledgerManager)
- .fetchAccountEntriesStream(Matchers.anyString(), Matchers.anyString(), Matchers.anyString());
+ .fetchAccountEntriesStream(Matchers.anyString(), Matchers.anyString(), Matchers.anyString(), Matchers.eq("ASC"));
checkStateTransfer(
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index 6be1dae..eb7c4e0 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -79,6 +79,7 @@
individualLendingRequiredAccounts.add(CUSTOMER_LOAN);
individualLendingRequiredAccounts.add(PENDING_DISBURSAL);
individualLendingRequiredAccounts.add(LOAN_FUNDS_SOURCE);
+ individualLendingRequiredAccounts.add(LOANS_PAYABLE);
individualLendingRequiredAccounts.add(PROCESSING_FEE_INCOME);
individualLendingRequiredAccounts.add(ORIGINATION_FEE_INCOME);
individualLendingRequiredAccounts.add(DISBURSEMENT_FEE_INCOME);
@@ -126,6 +127,28 @@
ENTRY,
DISBURSEMENT_FEE_INCOME);
+ 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(ENTRY);
+ disbursePayment.setToAccountDesignator(CUSTOMER_LOAN);
+ disbursePayment.setProportionalTo(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
+ disbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ disbursePayment.setAmount(BigDecimal.ONE);
+
+ 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(LOANS_PAYABLE);
+ trackPrincipalDisbursePayment.setProportionalTo(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
+ trackPrincipalDisbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ trackPrincipalDisbursePayment.setAmount(BigDecimal.ONE);
+
//TODO: Make payable at time of ACCEPT_PAYMENT but accrued at MARK_LATE
final ChargeDefinition lateFee = charge(
LATE_FEE_NAME,
@@ -135,7 +158,7 @@
LATE_FEE_INCOME);
lateFee.setAccrueAction(Action.MARK_LATE.name());
lateFee.setAccrualAccountDesignator(LATE_FEE_ACCRUAL);
- lateFee.setProportionalTo(ChargeIdentifiers.PAYMENT_ID);
+ lateFee.setProportionalTo(ChargeIdentifiers.REPAYMENT_ID);
//TODO: Make multiple write off allowance charges.
final ChargeDefinition writeOffAllowanceCharge = charge(
@@ -151,12 +174,34 @@
Action.ACCEPT_PAYMENT,
BigDecimal.valueOf(0.05),
INTEREST_ACCRUAL,
- PENDING_DISBURSAL);
+ INTEREST_INCOME);
interestCharge.setForCycleSizeUnit(ChronoUnit.YEARS);
interestCharge.setAccrueAction(Action.APPLY_INTEREST.name());
interestCharge.setAccrualAccountDesignator(CUSTOMER_LOAN);
interestCharge.setProportionalTo(ChargeIdentifiers.RUNNING_BALANCE_DESIGNATOR);
+ final ChargeDefinition customerRepaymentCharge = new ChargeDefinition();
+ customerRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
+ customerRepaymentCharge.setIdentifier(REPAYMENT_ID);
+ customerRepaymentCharge.setName(REPAYMENT_NAME);
+ customerRepaymentCharge.setDescription(REPAYMENT_NAME);
+ customerRepaymentCharge.setFromAccountDesignator(CUSTOMER_LOAN);
+ customerRepaymentCharge.setToAccountDesignator(ENTRY);
+ customerRepaymentCharge.setProportionalTo(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
+ customerRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ customerRepaymentCharge.setAmount(BigDecimal.ONE);
+
+ final ChargeDefinition trackPrincipalRepaymentCharge = new ChargeDefinition();
+ trackPrincipalRepaymentCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
+ trackPrincipalRepaymentCharge.setIdentifier(TRACK_RETURN_PRINCIPAL_ID);
+ trackPrincipalRepaymentCharge.setName(TRACK_RETURN_PRINCIPAL_NAME);
+ trackPrincipalRepaymentCharge.setDescription(TRACK_RETURN_PRINCIPAL_NAME);
+ trackPrincipalRepaymentCharge.setFromAccountDesignator(LOANS_PAYABLE);
+ trackPrincipalRepaymentCharge.setToAccountDesignator(LOAN_FUNDS_SOURCE);
+ trackPrincipalRepaymentCharge.setProportionalTo(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
+ trackPrincipalRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ trackPrincipalRepaymentCharge.setAmount(BigDecimal.ONE);
+
final ChargeDefinition disbursementReturnCharge = charge(
RETURN_DISBURSEMENT_NAME,
Action.CLOSE,
@@ -169,9 +214,13 @@
ret.add(loanOriginationFee);
ret.add(loanFundsAllocation);
ret.add(disbursementFee);
+ ret.add(disbursePayment);
+ ret.add(trackPrincipalDisbursePayment);
ret.add(lateFee);
ret.add(writeOffAllowanceCharge);
ret.add(interestCharge);
+ ret.add(customerRepaymentCharge);
+ ret.add(trackPrincipalRepaymentCharge);
ret.add(disbursementReturnCharge);
return ret;
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 4e8fe11..41c3227 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
@@ -35,7 +35,9 @@
import io.mifos.portfolio.api.v1.domain.CostComponent;
import io.mifos.portfolio.api.v1.events.EventConstants;
import io.mifos.portfolio.service.internal.mapper.CaseMapper;
-import io.mifos.portfolio.service.internal.repository.*;
+import io.mifos.portfolio.service.internal.repository.CaseEntity;
+import io.mifos.portfolio.service.internal.repository.CaseRepository;
+import io.mifos.portfolio.service.internal.repository.TaskInstanceRepository;
import io.mifos.portfolio.service.internal.util.AccountingAdapter;
import io.mifos.portfolio.service.internal.util.ChargeInstance;
import org.springframework.beans.factory.annotation.Autowired;
@@ -159,7 +161,8 @@
public IndividualLoanCommandEvent process(final ApproveCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.APPROVE);
checkIfTasksAreOutstanding(dataContextOfAction, Action.APPROVE);
@@ -296,7 +299,7 @@
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
- productIdentifier, caseIdentifier, null);
+ productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.ACCEPT_PAYMENT);
checkIfTasksAreOutstanding(dataContextOfAction, Action.ACCEPT_PAYMENT);
@@ -312,7 +315,7 @@
= getRequestedChargeAmounts(command.getCommand().getCostComponents());
final BigDecimal sumOfAdjustments = costComponentsForRepaymentPeriod.stream()
- .filter(entry -> entry.getKey().getIdentifier().equals(ChargeIdentifiers.PAYMENT_ID))
+ .filter(entry -> entry.getKey().getIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID))
.map(entry -> getChargeAmount(
requestedChargeAmounts.get(entry.getKey().getIdentifier()),
entry.getValue().getAmount()))
@@ -335,14 +338,6 @@
dataContextOfAction.getMessageForCharge(Action.ACCEPT_PAYMENT),
Action.ACCEPT_PAYMENT.getTransactionType());
- final BigDecimal newBalance = costComponentsForRepaymentPeriod.getRunningBalance()
- .add(sumOfAdjustments);
- if (newBalance.compareTo(BigDecimal.ZERO) <= 0) {
- final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
- //TODO: customerCase.setCurrentState(Case.State.CLOSED.name());
- caseRepository.save(customerCase);
- }
-
return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier());
}
@@ -352,7 +347,8 @@
public IndividualLoanCommandEvent process(final WriteOffCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.WRITE_OFF);
checkIfTasksAreOutstanding(dataContextOfAction, Action.WRITE_OFF);
@@ -369,7 +365,8 @@
public IndividualLoanCommandEvent process(final CloseCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.CLOSE);
checkIfTasksAreOutstanding(dataContextOfAction, Action.CLOSE);
@@ -386,7 +383,8 @@
public IndividualLoanCommandEvent process(final RecoverCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
+ final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
+ productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.RECOVER);
checkIfTasksAreOutstanding(dataContextOfAction, Action.RECOVER);
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 573a405..439141a 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
@@ -40,6 +40,7 @@
import java.time.ZoneId;
import java.util.*;
import java.util.function.BiFunction;
+import java.util.stream.Collectors;
/**
* @author Myrle Krantz
@@ -111,9 +112,9 @@
case MARK_LATE:
return getCostComponentsForMarkLate(dataContextOfAction);
case WRITE_OFF:
- return getCostComponentsForMarkLate(dataContextOfAction);
+ return getCostComponentsForWriteOff(dataContextOfAction);
case RECOVER:
- return getCostComponentsForMarkLate(dataContextOfAction);
+ return getCostComponentsForRecover(dataContextOfAction);
default:
throw ServiceException.internalError("Invalid action: ''{0}''.", action.name());
}
@@ -125,13 +126,15 @@
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.OPEN, today()));
final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
- productIdentifier, minorCurrencyUnitDigits, BigDecimal.ZERO, scheduledActions);
+ productIdentifier, scheduledActions);
return getCostComponentsForScheduledCharges(
- scheduledCharges,
- caseParameters.getMaximumBalance(),
- BigDecimal.ZERO,
- minorCurrencyUnitDigits);
+ Collections.emptyMap(),
+ scheduledCharges,
+ caseParameters.getMaximumBalance(),
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ minorCurrencyUnitDigits);
}
public CostComponentsForRepaymentPeriod getCostComponentsForDeny(final DataContextOfAction dataContextOfAction) {
@@ -140,12 +143,14 @@
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.DENY, today()));
final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
- productIdentifier, minorCurrencyUnitDigits, BigDecimal.ZERO, scheduledActions);
+ productIdentifier, scheduledActions);
return getCostComponentsForScheduledCharges(
+ Collections.emptyMap(),
scheduledCharges,
caseParameters.getMaximumBalance(),
BigDecimal.ZERO,
+ BigDecimal.ZERO,
minorCurrencyUnitDigits);
}
@@ -156,28 +161,57 @@
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.APPROVE, today()));
final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
- productIdentifier, minorCurrencyUnitDigits, BigDecimal.ZERO, scheduledActions);
+ productIdentifier, scheduledActions);
return getCostComponentsForScheduledCharges(
- scheduledCharges,
- caseParameters.getMaximumBalance(),
- BigDecimal.ZERO,
- minorCurrencyUnitDigits);
+ Collections.emptyMap(),
+ scheduledCharges,
+ caseParameters.getMaximumBalance(),
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ minorCurrencyUnitDigits);
}
public CostComponentsForRepaymentPeriod getCostComponentsForDisburse(final DataContextOfAction dataContextOfAction) {
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
+ final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+
+
+ final Optional<LocalDateTime> optionalStartOfTerm = accountingAdapter.getDateOfOldestEntryContainingMessage(
+ customerLoanAccountIdentifier,
+ dataContextOfAction.getMessageForCharge(Action.DISBURSE));
final CaseParameters caseParameters = dataContextOfAction.getCaseParameters();
final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.DISBURSE, today()));
final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
- productIdentifier, minorCurrencyUnitDigits, BigDecimal.ZERO, scheduledActions);
+ productIdentifier, scheduledActions);
+
+
+ final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledCharges.stream()
+ .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x, Action.DISBURSE)));
+
+ final Map<ChargeDefinition, CostComponent> accruedCostComponents =
+ optionalStartOfTerm.map(startOfTerm ->
+ chargesSplitIntoScheduledAndAccrued.get(true)
+ .stream()
+ .map(ScheduledCharge::getChargeDefinition)
+ .collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
+ chargeDefinition -> getAccruedCostComponentToApply(
+ dataContextOfAction,
+ designatorToAccountIdentifierMapper,
+ startOfTerm.toLocalDate(),
+ chargeDefinition)))).orElse(Collections.emptyMap());
return getCostComponentsForScheduledCharges(
- scheduledCharges,
- caseParameters.getMaximumBalance(),
- BigDecimal.ZERO,
- minorCurrencyUnitDigits);
+ accruedCostComponents,
+ chargesSplitIntoScheduledAndAccrued.get(false),
+ caseParameters.getMaximumBalance(),
+ currentBalance,
+ BigDecimal.ZERO,//TODO: This needs to be provided by the user.
+ minorCurrencyUnitDigits);
}
public CostComponentsForRepaymentPeriod getCostComponentsForApplyInterest(
@@ -188,6 +222,8 @@
final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
+ final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
+
final CaseParameters caseParameters = dataContextOfAction.getCaseParameters();
final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
@@ -196,14 +232,23 @@
final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
productIdentifier,
- minorCurrencyUnitDigits,
- currentBalance,
Collections.singletonList(interestAction));
+ final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledCharges.stream()
+ .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x, Action.APPLY_INTEREST)));
+
+ final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
+ .stream()
+ .map(ScheduledCharge::getChargeDefinition)
+ .collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
+ chargeDefinition -> getAccruedCostComponentToApply(dataContextOfAction, designatorToAccountIdentifierMapper, startOfTerm, chargeDefinition)));
+
return getCostComponentsForScheduledCharges(
- scheduledCharges,
+ accruedCostComponents,
+ chargesSplitIntoScheduledAndAccrued.get(false),
caseParameters.getMaximumBalance(),
currentBalance,
+ BigDecimal.ZERO,
minorCurrencyUnitDigits);
}
@@ -215,44 +260,67 @@
final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier);
- final String interestAccrualAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.INTEREST_ACCRUAL);
final LocalDate startOfTerm = getStartOfTermOrThrow(dataContextOfAction, customerLoanAccountIdentifier);
- final BigDecimal interestAccrued = accountingAdapter.sumMatchingEntriesSinceDate(
- interestAccrualAccountIdentifier,
- startOfTerm,
- dataContextOfAction.getMessageForCharge(Action.APPLY_INTEREST));
- final BigDecimal interestApplied = accountingAdapter.sumMatchingEntriesSinceDate(
- interestAccrualAccountIdentifier,
- startOfTerm,
- dataContextOfAction.getMessageForCharge(Action.ACCEPT_PAYMENT));
- final BigDecimal interestOutstanding = interestAccrued.subtract(interestApplied);
final CaseParameters caseParameters = dataContextOfAction.getCaseParameters();
final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
- final List<ScheduledAction> scheduledActions
- = ScheduledActionHelpers.getNextScheduledActionForDisbursedLoan(
- startOfTerm,
- dataContextOfAction.getCustomerCase().getEndOfTerm().toLocalDate(),
- caseParameters,
- Action.ACCEPT_PAYMENT
- )
- .map(Collections::singletonList)
- .orElse(Collections.emptyList());
+ final ScheduledAction scheduledAction
+ = ScheduledActionHelpers.getNextScheduledPayment(
+ startOfTerm,
+ dataContextOfAction.getCustomerCase().getEndOfTerm().toLocalDate(),
+ caseParameters
+ );
final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
productIdentifier,
- minorCurrencyUnitDigits,
- currentBalance,
- scheduledActions);
+ Collections.singletonList(scheduledAction));
+
+ final Map<Boolean, List<ScheduledCharge>> chargesSplitIntoScheduledAndAccrued = scheduledCharges.stream()
+ .collect(Collectors.partitioningBy(x -> isAccruedChargeForAction(x, Action.ACCEPT_PAYMENT)));
+
+ final Map<ChargeDefinition, CostComponent> accruedCostComponents = chargesSplitIntoScheduledAndAccrued.get(true)
+ .stream()
+ .map(ScheduledCharge::getChargeDefinition)
+ .collect(Collectors.toMap(chargeDefinition -> chargeDefinition,
+ chargeDefinition -> getAccruedCostComponentToApply(dataContextOfAction, designatorToAccountIdentifierMapper, startOfTerm, chargeDefinition)));
return getCostComponentsForScheduledCharges(
- scheduledCharges,
+ accruedCostComponents,
+ chargesSplitIntoScheduledAndAccrued.get(false),
caseParameters.getMaximumBalance(),
currentBalance,
+ BigDecimal.ZERO,//TODO: This needs to be provided by the user, or calculated. ZERO is wrong.
minorCurrencyUnitDigits);
}
+ private static boolean isAccruedChargeForAction(final ScheduledCharge scheduledCharge, final Action action) {
+ return scheduledCharge.getChargeDefinition().getAccrueAction() != null &&
+ scheduledCharge.getChargeDefinition().getChargeAction().equals(action.name());
+ }
+
+ private CostComponent getAccruedCostComponentToApply(final DataContextOfAction dataContextOfAction,
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper,
+ final LocalDate startOfTerm,
+ final ChargeDefinition chargeDefinition) {
+ final CostComponent ret = new CostComponent();
+
+ final String accrualAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator());
+
+ final BigDecimal amountAccrued = accountingAdapter.sumMatchingEntriesSinceDate(
+ accrualAccountIdentifier,
+ startOfTerm,
+ dataContextOfAction.getMessageForCharge(Action.valueOf(chargeDefinition.getAccrueAction())));
+ final BigDecimal amountApplied = accountingAdapter.sumMatchingEntriesSinceDate(
+ accrualAccountIdentifier,
+ startOfTerm,
+ dataContextOfAction.getMessageForCharge(Action.valueOf(chargeDefinition.getChargeAction())));
+
+ ret.setChargeIdentifier(chargeDefinition.getIdentifier());
+ ret.setAmount(amountAccrued.subtract(amountApplied));
+ return ret;
+ }
+
private LocalDate getStartOfTermOrThrow(final DataContextOfAction dataContextOfAction,
final String customerLoanAccountIdentifier) {
final Optional<LocalDateTime> firstDisbursalDateTime = accountingAdapter.getDateOfOldestEntryContainingMessage(
@@ -268,34 +336,42 @@
private CostComponentsForRepaymentPeriod getCostComponentsForMarkLate(final DataContextOfAction dataContextOfAction) {
return null;
}
- public CostComponentsForRepaymentPeriod getCostComponentsForWriteOff(final DataContextOfAction dataContextOfAction) {
+ private CostComponentsForRepaymentPeriod getCostComponentsForWriteOff(final DataContextOfAction dataContextOfAction) {
return null;
}
private CostComponentsForRepaymentPeriod getCostComponentsForClose(final DataContextOfAction dataContextOfAction) {
return null;
}
- public CostComponentsForRepaymentPeriod getCostComponentsForRecover(final DataContextOfAction dataContextOfAction) {
+ private CostComponentsForRepaymentPeriod getCostComponentsForRecover(final DataContextOfAction dataContextOfAction) {
return null;
}
static CostComponentsForRepaymentPeriod getCostComponentsForScheduledCharges(
+ final Map<ChargeDefinition, CostComponent> accruedCostComponents,
final Collection<ScheduledCharge> scheduledCharges,
final BigDecimal maximumBalance,
final BigDecimal runningBalance,
+ final BigDecimal loanPaymentSize,
final int minorCurrencyUnitDigits) {
BigDecimal balanceAdjustment = BigDecimal.ZERO;
final Map<ChargeDefinition, CostComponent> costComponentMap = new HashMap<>();
- for (final ScheduledCharge scheduledCharge : scheduledCharges)
+
+ for (Map.Entry<ChargeDefinition, CostComponent> entry : accruedCostComponents.entrySet()) {
+ costComponentMap.put(entry.getKey(), entry.getValue());
+
+ if (chargeDefinitionTouchesCustomerLoanAccount(entry.getKey()))
+ balanceAdjustment = balanceAdjustment.add(entry.getValue().getAmount());
+ }
+
+ final Map<Boolean, List<ScheduledCharge>> partitionedCharges = scheduledCharges.stream()
+ .collect(Collectors.partitioningBy(CostComponentService::proportionalToPrincipalAdjustment));
+
+ for (final ScheduledCharge scheduledCharge : partitionedCharges.get(false))
{
final CostComponent costComponent = costComponentMap
.computeIfAbsent(scheduledCharge.getChargeDefinition(),
- chargeIdentifier -> {
- final CostComponent ret = new CostComponent();
- ret.setChargeIdentifier(scheduledCharge.getChargeDefinition().getIdentifier());
- ret.setAmount(BigDecimal.ZERO);
- return ret;
- });
+ chargeIdentifier -> constructEmptyCostComponent(scheduledCharge));
final BigDecimal chargeAmount = howToApplyScheduledChargeToBalance(scheduledCharge)
.apply(maximumBalance, runningBalance)
@@ -305,12 +381,46 @@
costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
}
+ final BigDecimal principalAdjustment = loanPaymentSize.subtract(balanceAdjustment);
+ for (final ScheduledCharge scheduledCharge : partitionedCharges.get(true))
+ {
+ final CostComponent costComponent = costComponentMap
+ .computeIfAbsent(scheduledCharge.getChargeDefinition(),
+ chargeIdentifier -> constructEmptyCostComponent(scheduledCharge));
+
+ final BigDecimal chargeAmount = applyPrincipalAdjustmentCharge(scheduledCharge, principalAdjustment)
+ .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
+ if (chargeDefinitionTouchesCustomerLoanAccount(scheduledCharge.getChargeDefinition()))
+ balanceAdjustment = balanceAdjustment.add(chargeAmount);
+ costComponent.setAmount(costComponent.getAmount().add(chargeAmount));
+ }
+
return new CostComponentsForRepaymentPeriod(
runningBalance,
costComponentMap,
balanceAdjustment);
}
+ private static BigDecimal applyPrincipalAdjustmentCharge(
+ final ScheduledCharge scheduledCharge,
+ final BigDecimal principalAdjustment) {
+ return scheduledCharge.getChargeDefinition().getAmount().multiply(principalAdjustment);
+ }
+
+ private static CostComponent constructEmptyCostComponent(ScheduledCharge scheduledCharge) {
+ final CostComponent ret = new CostComponent();
+ ret.setChargeIdentifier(scheduledCharge.getChargeDefinition().getIdentifier());
+ ret.setAmount(BigDecimal.ZERO);
+ return ret;
+ }
+
+ private static boolean proportionalToPrincipalAdjustment(final ScheduledCharge scheduledCharge) {
+ if (!scheduledCharge.getChargeDefinition().getChargeMethod().equals(ChargeDefinition.ChargeMethod.PROPORTIONAL))
+ return false;
+ final String proportionalTo = scheduledCharge.getChargeDefinition().getProportionalTo();
+ return proportionalTo != null && proportionalTo.equals(ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR);
+ }
+
private static BiFunction<BigDecimal, BigDecimal, BigDecimal> howToApplyScheduledChargeToBalance(
final ScheduledCharge scheduledCharge)
{
@@ -329,6 +439,8 @@
return (maximumBalance, runningBalance) ->
PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, RUNNING_CALCULATION_PRECISION)
.multiply(maximumBalance);
+ case ChargeIdentifiers.PRINCIPAL_ADJUSTMENT_DESIGNATOR: //This is handled elsewhere.
+ throw new IllegalStateException("A principal adjustment charge should not be passed to the same application function as the other charges.");
default:
//TODO: correctly implement charges which are proportionate to other charges.
return (maximumBalance, runningBalance) ->
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 5dcf51a..b7a09e2 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
@@ -19,7 +19,6 @@
import io.mifos.individuallending.api.v1.domain.caseinstance.ChargeName;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
-import io.mifos.individuallending.api.v1.domain.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;
@@ -39,11 +38,7 @@
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.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;
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.REPAYMENT_ID;
/**
* @author Myrle Krantz
@@ -76,9 +71,17 @@
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(initialDisbursalDate, caseParameters);
- final List<ScheduledCharge> scheduledCharges = getScheduledCharges(productIdentifier, minorCurrencyUnitDigits, caseParameters.getMaximumBalance(), scheduledActions);
+ final List<ScheduledCharge> scheduledCharges = getScheduledCharges(productIdentifier, scheduledActions);
- final List<PlannedPayment> plannedPaymentsElements = getPlannedPaymentsElements(caseParameters.getMaximumBalance(), minorCurrencyUnitDigits, scheduledCharges);
+ final int precision = caseParameters.getMaximumBalance().precision() + minorCurrencyUnitDigits + EXTRA_PRECISION;
+ final Map<Period, BigDecimal> accrualRatesByPeriod
+ = periodChargeCalculator.getPeriodAccrualRates(scheduledCharges,
+ precision);
+
+ final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream().collect(RateCollectors.geometricMean(precision));
+ final BigDecimal loanPaymentSize = loanPaymentInContextOfAccruedInterest(caseParameters.getMaximumBalance(), accrualRatesByPeriod.size(), geometricMeanAccrualRate);
+
+ final List<PlannedPayment> plannedPaymentsElements = getPlannedPaymentsElements(caseParameters.getMaximumBalance(), minorCurrencyUnitDigits, scheduledCharges, loanPaymentSize);
final Set<ChargeName> chargeNames = scheduledCharges.stream()
.map(IndividualLoanService::chargeNameFromChargeDefinition)
@@ -112,8 +115,6 @@
List<ScheduledCharge> getScheduledCharges(
final String productIdentifier,
- final int minorCurrencyUnitDigits,
- final BigDecimal initialBalance,
final @Nonnull List<ScheduledAction> scheduledActions) {
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction
= chargeDefinitionService.getChargeDefinitionsMappedByChargeAction(productIdentifier);
@@ -121,36 +122,10 @@
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction
= chargeDefinitionService.getChargeDefinitionsMappedByAccrueAction(productIdentifier);
- final ChargeDefinition acceptPaymentDefinition = getPaymentChargeDefinition();
-
- final List<ScheduledCharge> scheduledCharges = getScheduledCharges(
+ return getScheduledCharges(
scheduledActions,
chargeDefinitionsMappedByChargeAction,
- chargeDefinitionsMappedByAccrueAction,
- acceptPaymentDefinition);
- int digitsInInitialBalance = initialBalance.precision();
- final Map<Period, BigDecimal> accrualRatesByPeriod
- = periodChargeCalculator.getPeriodAccrualRates(scheduledCharges,
- digitsInInitialBalance + minorCurrencyUnitDigits + EXTRA_PRECISION);
-
- if (accrualRatesByPeriod.size() != 0) {
- final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream().collect(RateCollectors.geometricMean(digitsInInitialBalance + minorCurrencyUnitDigits + EXTRA_PRECISION));
- acceptPaymentDefinition.setAmount(loanPaymentInContextOfAccruedInterest(initialBalance, accrualRatesByPeriod.size(), geometricMeanAccrualRate));
- }
- else
- acceptPaymentDefinition.setAmount(initialBalance);
- return scheduledCharges;
- }
-
- private ChargeDefinition getPaymentChargeDefinition() {
- final ChargeDefinition ret = new ChargeDefinition();
- ret.setChargeAction(Action.ACCEPT_PAYMENT.name());
- ret.setIdentifier(PAYMENT_ID);
- ret.setName(PAYMENT_NAME);
- ret.setFromAccountDesignator(CUSTOMER_LOAN);
- ret.setToAccountDesignator(PENDING_DISBURSAL);
- ret.setChargeMethod(FIXED);
- return ret;
+ chargeDefinitionsMappedByAccrueAction);
}
private static class ScheduledChargeComparator implements Comparator<ScheduledCharge>
@@ -168,9 +143,10 @@
}
static private List<PlannedPayment> getPlannedPaymentsElements(
- final BigDecimal initialBalance,
- final int minorCurrencyUnitDigits,
- final List<ScheduledCharge> scheduledCharges) {
+ final BigDecimal initialBalance,
+ final int minorCurrencyUnitDigits,
+ final List<ScheduledCharge> scheduledCharges,
+ final BigDecimal loanPaymentSize) {
final Map<Period, SortedSet<ScheduledCharge>> orderedScheduledChargesGroupedByPeriod
= scheduledCharges.stream()
.collect(Collectors.groupingBy(scheduledCharge -> {
@@ -196,7 +172,13 @@
{
final SortedSet<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
- CostComponentService.getCostComponentsForScheduledCharges(scheduledChargesInPeriod, balance, balance, minorCurrencyUnitDigits);
+ CostComponentService.getCostComponentsForScheduledCharges(
+ Collections.emptyMap(),
+ scheduledChargesInPeriod,
+ balance,
+ balance,
+ loanPaymentSize,
+ minorCurrencyUnitDigits);
final PlannedPayment plannedPayment = new PlannedPayment();
plannedPayment.setCostComponents(new ArrayList<>(costComponentsForRepaymentPeriod.getCostComponents().values()));
@@ -209,7 +191,7 @@
{
final PlannedPayment lastPayment = plannedPayments.get(plannedPayments.size() - 1);
final Optional<CostComponent> lastPaymentPayment = lastPayment.getCostComponents().stream()
- .filter(x -> x.getChargeIdentifier().equals(PAYMENT_ID)).findAny();
+ .filter(x -> x.getChargeIdentifier().equals(REPAYMENT_ID)).findAny();
lastPaymentPayment.ifPresent(x -> {
x.setAmount(x.getAmount().subtract(lastPayment.getRemainingPrincipal()));
lastPayment.setRemainingPrincipal(BigDecimal.ZERO.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN));
@@ -231,14 +213,12 @@
private List<ScheduledCharge> getScheduledCharges(final List<ScheduledAction> scheduledActions,
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
- final ChargeDefinition acceptPaymentDefinition) {
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction) {
return scheduledActions.stream()
.flatMap(scheduledAction ->
getChargeDefinitionStream(
chargeDefinitionsMappedByChargeAction,
chargeDefinitionsMappedByAccrueAction,
- acceptPaymentDefinition,
scheduledAction)
.map(chargeDefinition -> new ScheduledCharge(scheduledAction, chargeDefinition)))
.collect(Collectors.toList());
@@ -247,7 +227,6 @@
private Stream<ChargeDefinition> getChargeDefinitionStream(
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByChargeAction,
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
- final ChargeDefinition acceptPaymentDefinition,
final ScheduledAction scheduledAction) {
final List<ChargeDefinition> chargeMappingList = chargeDefinitionsMappedByChargeAction
.get(scheduledAction.action.name());
@@ -255,18 +234,12 @@
if (chargeMapping == null)
chargeMapping = Stream.empty();
- if (scheduledAction.action == Action.valueOf(acceptPaymentDefinition.getChargeAction()))
- chargeMapping = Stream.concat(chargeMapping, Stream.of(acceptPaymentDefinition));
-
final List<ChargeDefinition> accrueMappingList = chargeDefinitionsMappedByAccrueAction
.get(scheduledAction.action.name());
Stream<ChargeDefinition> accrueMapping = accrueMappingList == null ? Stream.empty() : accrueMappingList.stream();
if (accrueMapping == null)
accrueMapping = Stream.empty();
- if ((acceptPaymentDefinition.getAccrueAction() != null) && (scheduledAction.action == Action.valueOf(acceptPaymentDefinition.getAccrueAction())))
- accrueMapping = Stream.concat(chargeMapping, Stream.of(acceptPaymentDefinition));
-
return Stream.concat(accrueMapping, chargeMapping);
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java
index 1d85ecb..323e3f1 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledAction.java
@@ -58,6 +58,10 @@
this.repaymentPeriod = null;
}
+ boolean actionIsOnOrAfter(final LocalDate date) {
+ return when.compareTo(date) > 0;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
index e310af1..434414b 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/ScheduledActionHelpers.java
@@ -20,10 +20,7 @@
import io.mifos.portfolio.api.v1.domain.PaymentCycle;
import javax.annotation.Nonnull;
-import java.time.DayOfWeek;
-import java.time.LocalDate;
-import java.time.YearMonth;
-import java.time.ZoneId;
+import java.time.*;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
@@ -57,19 +54,17 @@
.collect(Collectors.toList());
}
- public static Optional<ScheduledAction> getNextScheduledActionForDisbursedLoan(final @Nonnull LocalDate startOfTerm,
- final @Nonnull LocalDate endOfTerm,
- final @Nonnull CaseParameters caseParameters,
- final @Nonnull Action action) {
- if (preTermActions().anyMatch(x -> action == x))
- throw new IllegalStateException("Should not be calling getNextScheduledActionsForDisbursedLoan with an action which occurs before disbursement.");
+ public static ScheduledAction getNextScheduledPayment(final @Nonnull LocalDate startOfTerm,
+ final @Nonnull LocalDate endOfTerm,
+ final @Nonnull CaseParameters caseParameters) {
+ final LocalDate now = LocalDate.now(Clock.systemUTC());
+ final LocalDate effectiveEndOfTerm = now.isAfter(endOfTerm) ? now : endOfTerm;
- final LocalDate now = LocalDate.now(ZoneId.of("UTC"));
- return getHypotheticalScheduledActionsForDisbursedLoan(startOfTerm, endOfTerm, caseParameters)
- .filter(x -> x.action.equals(action))
- .filter(x -> x.actionPeriod != null && x.actionPeriod.containsDate(now))
- .sorted(Comparator.comparing(x -> x.actionPeriod))
- .findFirst();
+ return getHypotheticalScheduledActionsForDisbursedLoan(startOfTerm, effectiveEndOfTerm, caseParameters)
+ .filter(x -> x.action.equals(Action.ACCEPT_PAYMENT))
+ .filter(x -> x.actionIsOnOrAfter(now))
+ .findFirst()
+ .orElseGet(() -> new ScheduledAction(Action.ACCEPT_PAYMENT, now));
}
private static Stream<ScheduledAction> getHypotheticalScheduledActionsForDisbursedLoan(
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 f4bff53..adffc64 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
@@ -78,7 +78,7 @@
else if (identifier.equals(PROCESSING_FEE_ID))
return MAXIMUM_BALANCE_DESIGNATOR;
else if (identifier.equals(LATE_FEE_ID))
- return PAYMENT_ID;
+ return REPAYMENT_ID;
else
return RUNNING_BALANCE_DESIGNATOR;
}
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 d504b23..ad6a55d 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
@@ -44,6 +44,7 @@
*/
@Component
public class AccountingAdapter {
+
public enum IdentifierType {LEDGER, ACCOUNT}
private final LedgerManager ledgerManager;
@@ -87,16 +88,31 @@
final LocalDateTime accountCreatedOn = DateConverter.fromIsoString(account.getCreatedOn());
final DateRange fromAccountCreationUntilNow = oneSidedDateRange(accountCreatedOn.toLocalDate());
- return ledgerManager.fetchAccountEntriesStream(accountIdentifier, fromAccountCreationUntilNow.toString(), message)
+ return ledgerManager.fetchAccountEntriesStream(accountIdentifier, fromAccountCreationUntilNow.toString(), message, "ASC")
.findFirst()
.map(AccountEntry::getTransactionDate)
.map(DateConverter::fromIsoString);
}
+ public List<LocalDateTime> getDatesOfMostRecentTwoEntriesContainingMessage(final String accountIdentifier,
+ final String message) {
+
+ final Account account = ledgerManager.findAccount(accountIdentifier);
+ final LocalDateTime accountCreatedOn = DateConverter.fromIsoString(account.getCreatedOn());
+ final DateRange fromAccountCreationUntilNow = oneSidedDateRange(accountCreatedOn.toLocalDate());
+
+ return ledgerManager.fetchAccountEntriesStream(accountIdentifier, fromAccountCreationUntilNow.toString(), message, "DESC")
+ .limit(2)
+ .map(AccountEntry::getTransactionDate)
+ .map(DateConverter::fromIsoString)
+ .collect(Collectors.toList());
+ }
+
public BigDecimal sumMatchingEntriesSinceDate(final String accountIdentifier, final LocalDate startDate, final String message)
{
final DateRange fromLastPaymentUntilNow = oneSidedDateRange(startDate);
- return ledgerManager.fetchAccountEntriesStream(accountIdentifier, fromLastPaymentUntilNow.toString(), message)
+ final Stream<AccountEntry> accountEntriesStream = ledgerManager.fetchAccountEntriesStream(accountIdentifier, fromLastPaymentUntilNow.toString(), message, "ASC");
+ return accountEntriesStream
.map(AccountEntry::getAmount)
.map(BigDecimal::valueOf).reduce(BigDecimal.ZERO, BigDecimal::add);
}
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 f194269..f672c6d 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
@@ -87,7 +87,7 @@
private CaseParameters caseParameters;
private LocalDate initialDisbursementDate;
private Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction;
- private Set<String> expectedChargeIdentifiers = new HashSet<>(Arrays.asList(ChargeIdentifiers.INTEREST_ID, ChargeIdentifiers.PAYMENT_ID));
+ private Set<String> expectedChargeIdentifiers = new HashSet<>(Arrays.asList(ChargeIdentifiers.INTEREST_ID, ChargeIdentifiers.REPAYMENT_ID));
private Map<ActionDatePair, List<ChargeDefinition>> chargeDefinitionsForActions = new HashMap<>();
//This is an abuse of the ChargeInstance since everywhere else it's intended to contain account identifiers and not
//account designators. Don't copy the code around charge instances in this test without thinking about what you're
@@ -165,17 +165,18 @@
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, 0, null, null));
//I know: this is cheating in a unit test. But I really didn't want to put this data together by hand.
-
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = new HashMap<>();
- chargeDefinitionsMappedByAction.put(Action.APPLY_INTEREST.name(), getInterestChargeDefinition(0.01, ChronoUnit.YEARS));
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = constructCharges(0.01);
final ChargeDefinition processingFeeCharge = getFixedSingleChargeDefinition(10.0, Action.OPEN, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME);
chargeDefinitionsMappedByAction.put(Action.OPEN.name(), Collections.singletonList(processingFeeCharge));
- ChargeDefinition loanOriginationFeeCharge = getFixedSingleChargeDefinition(100.0, Action.APPROVE, LOAN_ORIGINATION_FEE_ID, AccountDesignators.ORIGINATION_FEE_INCOME);
- ChargeDefinition loanFundsAllocationCharge = getProportionalSingleChargeDefinition(1.0, Action.APPROVE, LOAN_FUNDS_ALLOCATION_ID, AccountDesignators.LOAN_FUNDS_SOURCE, AccountDesignators.PENDING_DISBURSAL);
- chargeDefinitionsMappedByAction.put(Action.APPROVE.name(),
- Arrays.asList(
- loanOriginationFeeCharge,
- loanFundsAllocationCharge));
+ final ChargeDefinition loanOriginationFeeCharge = getFixedSingleChargeDefinition(100.0, Action.APPROVE, LOAN_ORIGINATION_FEE_ID, AccountDesignators.ORIGINATION_FEE_INCOME);
+ final List<ChargeDefinition> existingApprovalCharges = chargeDefinitionsMappedByAction.get(Action.APPROVE.name());
+ final List<ChargeDefinition> approvalChargesWithLoanOriginationFeeReplaced = existingApprovalCharges.stream().map(x -> {
+ if (x.getIdentifier().equals(LOAN_ORIGINATION_FEE_ID))
+ return loanOriginationFeeCharge;
+ else
+ return x;
+ }).collect(Collectors.toList());
+ chargeDefinitionsMappedByAction.put(Action.APPROVE.name(), approvalChargesWithLoanOriginationFeeReplaced);
return new TestCase("simpleCase")
.minorCurrencyUnitDigits(2)
@@ -187,7 +188,7 @@
.expectAdditionalChargeIdentifier(LOAN_ORIGINATION_FEE_ID)
.expectChargeInstancesForActionDatePair(Action.OPEN, initialDisbursementDate, Collections.singletonList(processingFeeCharge))
.expectChargeInstancesForActionDatePair(Action.APPROVE, initialDisbursementDate,
- Arrays.asList(loanOriginationFeeCharge, loanFundsAllocationCharge));
+ Collections.singletonList(loanOriginationFeeCharge));
}
private static TestCase yearLoanTestCase()
@@ -199,8 +200,7 @@
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.MONTHS, 1, 0, null, null));
caseParameters.setMaximumBalance(BigDecimal.valueOf(200000));
- final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = new HashMap<>();
- chargeDefinitionsMappedByAction.put(Action.APPLY_INTEREST.name(), getInterestChargeDefinition(0.10, ChronoUnit.YEARS));
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = constructCharges(0.10);
return new TestCase("yearLoanTestCase")
.minorCurrencyUnitDigits(3)
@@ -217,20 +217,25 @@
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, 1, 0, 0));
caseParameters.setMaximumBalance(BigDecimal.valueOf(2000));
- final List<ChargeDefinition> defaultLoanCharges = IndividualLendingPatternFactory.defaultIndividualLoanCharges();
-
- 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));
+ final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = constructCharges(0.05);
return new TestCase("chargeDefaultsCase")
.minorCurrencyUnitDigits(2)
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.chargeDefinitionsMappedByAction(chargeDefinitionsMappedByAction)
- .expectedChargeIdentifiers(new HashSet<>(Arrays.asList(PROCESSING_FEE_ID, LOAN_FUNDS_ALLOCATION_ID, RETURN_DISBURSEMENT_ID, LOAN_ORIGINATION_FEE_ID, INTEREST_ID, DISBURSEMENT_FEE_ID, PAYMENT_ID)));
+ .expectedChargeIdentifiers(new HashSet<>(Arrays.asList(PROCESSING_FEE_ID, LOAN_FUNDS_ALLOCATION_ID, RETURN_DISBURSEMENT_ID, LOAN_ORIGINATION_FEE_ID, INTEREST_ID, DISBURSEMENT_FEE_ID, REPAYMENT_ID)));
+ }
+
+ private static Map<String, List<ChargeDefinition>> constructCharges(final double interestRate) {
+ final List<ChargeDefinition> defaultLoanCharges = IndividualLendingPatternFactory.defaultIndividualLoanCharges();
+
+ 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(interestRate, ChronoUnit.YEARS));
+ return chargeDefinitionsMappedByAction;
}
private static List<ChargeDefinition> getInterestChargeDefinition(final double amount, final ChronoUnit forCycleSizeUnit) {
@@ -364,8 +369,6 @@
public void getScheduledCharges() {
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
final List<ScheduledCharge> scheduledCharges = testSubject.getScheduledCharges(testCase.productIdentifier,
- testCase.minorCurrencyUnitDigits,
- testCase.caseParameters.getMaximumBalance(),
scheduledActions);
final List<LocalDate> interestCalculationDates = scheduledCharges.stream()
@@ -382,12 +385,15 @@
final List<LocalDate> acceptPaymentDates = scheduledCharges.stream()
.filter(scheduledCharge -> scheduledCharge.getScheduledAction().action == Action.ACCEPT_PAYMENT)
- .filter(scheduledCharge -> scheduledCharge.getChargeDefinition().getIdentifier().equals(ChargeIdentifiers.PAYMENT_ID))
.map(scheduledCharge -> scheduledCharge.getScheduledAction().when)
.collect(Collectors.toList());
final long expectedAcceptPayments = scheduledActions.stream()
.filter(x -> x.action == Action.ACCEPT_PAYMENT).count();
- Assert.assertEquals("There should be no duplicate entries for payments", expectedAcceptPayments, acceptPaymentDates.size());
+ final List<ChargeDefinition> chargeDefinitionsMappedToAcceptPayment = testCase.chargeDefinitionsMappedByAction.get(Action.ACCEPT_PAYMENT.name());
+ final int numberOfChangeDefinitionsMappedToAcceptPayment = chargeDefinitionsMappedToAcceptPayment == null ? 0 : chargeDefinitionsMappedToAcceptPayment.size();
+ Assert.assertEquals("check for correct number of scheduled charges for accept payment",
+ expectedAcceptPayments*numberOfChangeDefinitionsMappedToAcceptPayment,
+ acceptPaymentDates.size());
final Map<ActionDatePair, Set<ChargeDefinition>> searchableScheduledCharges = scheduledCharges.stream()
.collect(
@@ -395,7 +401,7 @@
new ActionDatePair(scheduledCharge.getScheduledAction().action, scheduledCharge.getScheduledAction().when),
Collectors.mapping(ScheduledCharge::getChargeDefinition, Collectors.toSet())));
- testCase.chargeDefinitionsForActions.forEach((key, value) -> Assert.assertEquals(new HashSet<>(value), searchableScheduledCharges.get(key)));
+ testCase.chargeDefinitionsForActions.forEach((key, value) -> value.forEach(x -> Assert.assertTrue(searchableScheduledCharges.get(key).contains(x))));
}
private double percentDifference(final BigDecimal maxPayment, final BigDecimal minPayment) {
@@ -406,7 +412,7 @@
private Optional<BigDecimal> getCustomerRepayment(final PlannedPayment plannedPayment) {
final Optional<CostComponent> ret = plannedPayment.getCostComponents().stream()
- .filter(y -> y.getChargeIdentifier().equals(ChargeIdentifiers.PAYMENT_ID))
+ .filter(y -> y.getChargeIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID))
.findAny();
return ret.map(x -> x.getAmount().abs());
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java
new file mode 100644
index 0000000..ff22aea
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/ScheduledActionTest.java
@@ -0,0 +1,20 @@
+package io.mifos.individuallending.internal.service;
+
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.time.LocalDate;
+
+public class ScheduledActionTest {
+ @Test
+ public void actionIsOnOrBefore() {
+ final LocalDate today = LocalDate.now();
+ final LocalDate tomorrow = today.plusDays(1);
+ final LocalDate yesterday = today.minusDays(1);
+ final ScheduledAction testSubject = new ScheduledAction(Action.APPLY_INTEREST, today);
+ Assert.assertFalse(testSubject.actionIsOnOrAfter(today));
+ Assert.assertFalse(testSubject.actionIsOnOrAfter(tomorrow));
+ Assert.assertTrue(testSubject.actionIsOnOrAfter(yesterday));
+ }
+}
\ No newline at end of file