Merge pull request #32 from myrle-krantz/develop
straightening up task definitions in preparation for attacking task instances.
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 3814efe..35a7701 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
@@ -138,6 +138,7 @@
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
+ @ThrowsException(status = HttpStatus.CONFLICT, exception = ProductInUseException.class)
void createTaskDefinition(
@PathVariable("productidentifier") final String productIdentifier,
final TaskDefinition taskDefinition);
@@ -153,16 +154,25 @@
@PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier);
@RequestMapping(
- value = "/products/{productidentifier}/tasks/{taskdefinitionidentifier}",
- method = RequestMethod.PUT,
- produces = MediaType.ALL_VALUE,
- consumes = MediaType.APPLICATION_JSON_VALUE
- )
+ value = "/products/{productidentifier}/tasks/{taskdefinitionidentifier}",
+ method = RequestMethod.PUT,
+ produces = MediaType.ALL_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ThrowsException(status = HttpStatus.CONFLICT, exception = ProductInUseException.class)
void changeTaskDefinition(
- @PathVariable("productidentifier") final String productIdentifier,
- @PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier,
- final TaskDefinition taskDefinition);
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier,
+ final TaskDefinition taskDefinition);
+ @RequestMapping(
+ value = "/products/{productidentifier}/tasks/{taskdefinitionidentifier}",
+ method = RequestMethod.DELETE,
+ produces = MediaType.ALL_VALUE,
+ consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ThrowsException(status = HttpStatus.CONFLICT, exception = ProductInUseException.class)
+ void deleteTaskDefinition(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier);
@RequestMapping(
value = "/products/{productidentifier}/charges/",
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
index 708b65a..76eb969 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/domain/Command.java
@@ -15,6 +15,7 @@
*/
package io.mifos.portfolio.api.v1.domain;
+import javax.annotation.Nullable;
import javax.validation.Valid;
import java.util.List;
import java.util.Objects;
@@ -27,6 +28,10 @@
@Valid
private List<AccountAssignment> oneTimeAccountAssignments;
+ @Valid
+ @Nullable
+ private List<CostComponent> costComponents;
+
private String note;
private String createdOn;
private String createdBy;
@@ -42,6 +47,14 @@
this.oneTimeAccountAssignments = oneTimeAccountAssignments;
}
+ public List<CostComponent> getCostComponents() {
+ return costComponents;
+ }
+
+ public void setCostComponents(List<CostComponent> costComponents) {
+ this.costComponents = costComponents;
+ }
+
public String getNote() {
return note;
}
@@ -72,23 +85,23 @@
if (o == null || getClass() != o.getClass()) return false;
Command command = (Command) o;
return Objects.equals(oneTimeAccountAssignments, command.oneTimeAccountAssignments) &&
- Objects.equals(note, command.note) &&
- Objects.equals(createdOn, command.createdOn) &&
- Objects.equals(createdBy, command.createdBy);
+ Objects.equals(costComponents, command.costComponents) &&
+ Objects.equals(note, command.note);
}
@Override
public int hashCode() {
- return Objects.hash(oneTimeAccountAssignments, note, createdOn, createdBy);
+ return Objects.hash(oneTimeAccountAssignments, costComponents, note);
}
@Override
public String toString() {
return "Command{" +
- "oneTimeAccountAssignments=" + oneTimeAccountAssignments +
- ", note='" + note + '\'' +
- ", createdOn='" + createdOn + '\'' +
- ", createdBy='" + createdBy + '\'' +
- '}';
+ "oneTimeAccountAssignments=" + oneTimeAccountAssignments +
+ ", costComponents=" + costComponents +
+ ", note='" + note + '\'' +
+ ", createdOn='" + createdOn + '\'' +
+ ", createdBy='" + createdBy + '\'' +
+ '}';
}
}
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java b/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java
index 87ba261..777e819 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/events/EventConstants.java
@@ -31,6 +31,7 @@
String PUT_CASE = "put-case";
String POST_TASK_DEFINITION = "post-task-definition";
String PUT_TASK_DEFINITION = "put-task-definition";
+ String DELETE_TASK_DEFINITION = "delete-task-definition";
String POST_CHARGE_DEFINITION = "post-charge-definition";
String PUT_CHARGE_DEFINITION = "put-charge-definition";
String DELETE_PRODUCT_CHARGE_DEFINITION = "delete-product-charge-definition";
@@ -43,6 +44,7 @@
String SELECTOR_PUT_CASE = SELECTOR_NAME + " = '" + PUT_CASE + "'";
String SELECTOR_POST_TASK_DEFINITION = SELECTOR_NAME + " = '" + POST_TASK_DEFINITION + "'";
String SELECTOR_PUT_TASK_DEFINITION = SELECTOR_NAME + " = '" + PUT_TASK_DEFINITION + "'";
+ String SELECTOR_DELETE_TASK_DEFINITION = SELECTOR_NAME + " = '" + DELETE_TASK_DEFINITION + "'";
String SELECTOR_POST_CHARGE_DEFINITION = SELECTOR_NAME + " = '" + POST_CHARGE_DEFINITION + "'";
String SELECTOR_PUT_CHARGE_DEFINITION = SELECTOR_NAME + " = '" + PUT_CHARGE_DEFINITION + "'";
String SELECTOR_DELETE_PRODUCT_CHARGE_DEFINITION = SELECTOR_NAME + " = '" + DELETE_PRODUCT_CHARGE_DEFINITION + "'";
diff --git a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
index 4e27123..efca4f2 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -30,6 +30,7 @@
import io.mifos.portfolio.api.v1.events.CaseEvent;
import io.mifos.portfolio.api.v1.events.ChargeDefinitionEvent;
import io.mifos.portfolio.api.v1.events.EventConstants;
+import io.mifos.portfolio.api.v1.events.TaskDefinitionEvent;
import io.mifos.portfolio.service.config.PortfolioServiceConfiguration;
import io.mifos.portfolio.service.internal.util.AccountingAdapter;
import io.mifos.portfolio.service.internal.util.RhythmAdapter;
@@ -70,7 +71,9 @@
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
- classes = {AbstractPortfolioTest.TestConfiguration.class})
+ classes = {AbstractPortfolioTest.TestConfiguration.class},
+ properties = {"portfolio.bookInterestAsUser=interest_user", "portfolio.bookInterestInTimeSlot=0"}
+)
public class AbstractPortfolioTest extends SuiteTestEnvironment {
private static final String LOGGER_NAME = "test-logger";
@@ -111,12 +114,15 @@
private AutoUserContext userContext;
+ @SuppressWarnings({"SpringAutowiredFieldsWarningInspection", "SpringJavaAutowiringInspection"})
@Autowired
protected EventRecorder eventRecorder;
+ @SuppressWarnings("SpringAutowiredFieldsWarningInspection")
@Autowired
PortfolioManager portfolioManager;
+ @SuppressWarnings("SpringAutowiredFieldsWarningInspection")
@Autowired
IndividualLending individualLending;
@@ -127,6 +133,7 @@
@MockBean
LedgerManager ledgerManager;
+ @SuppressWarnings("SpringAutowiredFieldsWarningInspection")
@Autowired
@Qualifier(LOGGER_NAME)
Logger logger;
@@ -157,8 +164,7 @@
Product createAndEnableProduct() throws InterruptedException {
final Product product = createAdjustedProduct(x -> {});
- portfolioManager.enableProduct(product.getIdentifier(), true);
- Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_PRODUCT_ENABLE, product.getIdentifier()));
+ enableProduct(product);
return product;
}
@@ -202,7 +208,7 @@
Assert.assertTrue(eventRecorder.wait(event, new IndividualLoanCommandEvent(productIdentifier, caseIdentifier)));
final Case customerCase = portfolioManager.getCase(productIdentifier, caseIdentifier);
- Assert.assertEquals(customerCase.getCurrentState(), nextState.name());
+ Assert.assertEquals(nextState.name(), customerCase.getCurrentState());
}
boolean individualLoanCommandEventMatches(
@@ -250,4 +256,27 @@
return entryAccountAssignment;
}
+ TaskDefinition createTaskDefinition(Product product) throws InterruptedException {
+ final TaskDefinition taskDefinition = getTaskDefinition();
+ portfolioManager.createTaskDefinition(product.getIdentifier(), taskDefinition);
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
+ return taskDefinition;
+ }
+
+ TaskDefinition getTaskDefinition() {
+ final TaskDefinition ret = new TaskDefinition();
+ ret.setIdentifier(Fixture.generateUniqueIdentifer("task"));
+ ret.setDescription("But how do you feel about this?");
+ ret.setName("feep");
+ ret.setMandatory(false);
+ ret.setActions(new HashSet<>());
+ ret.setFourEyes(true);
+ return ret;
+ }
+
+ void enableProduct(final Product product) throws InterruptedException {
+ portfolioManager.enableProduct(product.getIdentifier(), true);
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_PRODUCT_ENABLE, product.getIdentifier()));
+ }
+
}
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 d838179..961d20d 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -17,6 +17,7 @@
import io.mifos.accounting.api.v1.client.LedgerManager;
import io.mifos.accounting.api.v1.domain.*;
+import io.mifos.core.lang.DateConverter;
import org.hamcrest.Description;
import org.mockito.AdditionalMatchers;
import org.mockito.ArgumentMatcher;
@@ -28,7 +29,10 @@
import javax.validation.Validation;
import javax.validation.Validator;
import java.math.BigDecimal;
+import java.time.Clock;
+import java.time.LocalDateTime;
import java.util.*;
+import java.util.stream.Stream;
import static org.mockito.Matchers.argThat;
@@ -37,6 +41,7 @@
*/
@SuppressWarnings("Duplicates")
class AccountingFixture {
+ private static final LocalDateTime universalCreationDate = LocalDateTime.of(2017, 7, 18, 15, 16, 43, 10);
private static final String INCOME_LEDGER_IDENTIFIER = "1000";
private static final String LOAN_INCOME_LEDGER_IDENTIFIER = "1100";
private static final String FEES_AND_CHARGES_LEDGER_IDENTIFIER = "1300";
@@ -54,7 +59,37 @@
static final String LOAN_INTEREST_ACCRUAL_ACCOUNT = "7810";
static final String CONSUMER_LOAN_INTEREST_ACCOUNT = "1103";
- static final Map<String, Account> accountMap = new HashMap<>();
+ static final Map<String, AccountData> accountMap = new HashMap<>();
+
+ private static class AccountData {
+ final Account account;
+ final List<AccountEntry> accountEntries = new ArrayList<>();
+
+ AccountData(final Account account) {
+ this.account = account;
+ }
+
+ void setBalance(final double balance) {
+ this.account.setBalance(balance);
+ }
+
+ void addAccountEntry(final double amount) {
+ final AccountEntry accountEntry = new AccountEntry();
+ accountEntry.setAmount(amount);
+ accountEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now(Clock.systemUTC())));
+ accountEntries.add(accountEntry);
+ }
+ }
+
+ private static void makeAccountResponsive(final Account account, final LocalDateTime creationDate, final LedgerManager ledgerManagerMock) {
+ account.setCreatedOn(DateConverter.toIsoString(creationDate));
+ final AccountData accountData = new AccountData(account);
+ accountMap.put(account.getIdentifier(), accountData);
+ Mockito.doAnswer(new AccountEntriesStreamAnswer(accountData))
+ .when(ledgerManagerMock)
+ .fetchAccountEntriesStream(Mockito.eq(account.getIdentifier()), Matchers.anyString(), Matchers.anyString());
+
+ }
private static Ledger cashLedger() {
@@ -62,6 +97,7 @@
ret.setIdentifier(CASH_LEDGER_IDENTIFIER);
ret.setParentLedgerIdentifier(ASSET_LEDGER_IDENTIFIER);
ret.setType(AccountType.ASSET.name());
+ ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
return ret;
}
@@ -69,6 +105,7 @@
final Ledger ret = new Ledger();
ret.setIdentifier(INCOME_LEDGER_IDENTIFIER);
ret.setType(AccountType.REVENUE.name());
+ ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
return ret;
}
@@ -77,6 +114,7 @@
ret.setIdentifier(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
ret.setParentLedgerIdentifier(INCOME_LEDGER_IDENTIFIER);
ret.setType(AccountType.REVENUE.name());
+ ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
return ret;
}
@@ -85,6 +123,7 @@
ret.setIdentifier(PENDING_DISBURSAL_LEDGER_IDENTIFIER);
ret.setParentLedgerIdentifier(CASH_LEDGER_IDENTIFIER);
ret.setType(AccountType.ASSET.name());
+ ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
return ret;
}
@@ -93,6 +132,7 @@
ret.setIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
ret.setParentLedgerIdentifier(CASH_LEDGER_IDENTIFIER);
ret.setType(AccountType.ASSET.name());
+ ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
return ret;
}
@@ -101,6 +141,7 @@
ret.setIdentifier(LOAN_INCOME_LEDGER_IDENTIFIER);
ret.setParentLedgerIdentifier(INCOME_LEDGER_IDENTIFIER);
ret.setType(AccountType.REVENUE.name());
+ ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
return ret;
}
@@ -110,64 +151,65 @@
ret.setIdentifier(ACCRUED_INCOME_LEDGER_IDENTIFIER);
ret.setParentLedgerIdentifier(ASSET_LEDGER_IDENTIFIER);
ret.setType(AccountType.ASSET.name());
+ ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
return ret;
}
- private static void loanFundsSourceAccount() {
+ private static Account loanFundsSourceAccount() {
final Account ret = new Account();
ret.setIdentifier(LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER);
ret.setLedger(CASH_LEDGER_IDENTIFIER);
ret.setType(AccountType.ASSET.name());
- accountMap.put(LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER, ret);
+ return ret;
}
- private static void processingFeeIncomeAccount() {
+ private static Account processingFeeIncomeAccount() {
final Account ret = new Account();
ret.setIdentifier(PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER);
ret.setLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
ret.setType(AccountType.REVENUE.name());
- accountMap.put(PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER, ret);
+ return ret;
}
- private static void loanOriginationFeesIncomeAccount() {
+ private static Account loanOriginationFeesIncomeAccount() {
final Account ret = new Account();
ret.setIdentifier(LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER);
ret.setLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
ret.setType(AccountType.REVENUE.name());
- accountMap.put(LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER, ret);
+ return ret;
}
- private static void disbursementFeeIncomeAccount() {
+ private static Account disbursementFeeIncomeAccount() {
final Account ret = new Account();
ret.setIdentifier(DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER);
ret.setLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
ret.setType(AccountType.REVENUE.name());
- accountMap.put(DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, ret);
+ return ret;
}
- private static void tellerOneAccount() {
+ private static Account tellerOneAccount() {
final Account ret = new Account();
ret.setIdentifier(TELLER_ONE_ACCOUNT_IDENTIFIER);
ret.setLedger(CASH_LEDGER_IDENTIFIER);
ret.setType(AccountType.ASSET.name());
- accountMap.put(TELLER_ONE_ACCOUNT_IDENTIFIER, ret);
+ return ret;
}
- private static void loanInterestAccrualAccount() {
+ private static Account loanInterestAccrualAccount() {
final Account ret = new Account();
ret.setIdentifier(LOAN_INTEREST_ACCRUAL_ACCOUNT);
ret.setLedger(ACCRUED_INCOME_LEDGER_IDENTIFIER);
ret.setType(AccountType.ASSET.name());
- accountMap.put(LOAN_INTEREST_ACCRUAL_ACCOUNT, ret);
+ return ret;
}
- private static void consumerLoanInterestAccount() {
+ private static Account consumerLoanInterestAccount() {
final Account ret = new Account();
ret.setIdentifier(CONSUMER_LOAN_INTEREST_ACCOUNT);
ret.setLedger(LOAN_INCOME_LEDGER_IDENTIFIER);
ret.setType(AccountType.REVENUE.name());
- accountMap.put(CONSUMER_LOAN_INTEREST_ACCOUNT, ret);
+ return ret;
}
private static AccountPage customerLoanAccountsPage() {
@@ -188,8 +230,10 @@
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");
@@ -295,29 +339,42 @@
private static class FindAccountAnswer implements Answer {
@Override
- public Account answer(InvocationOnMock invocation) throws Throwable {
+ public Account answer(final InvocationOnMock invocation) throws Throwable {
final String identifier = invocation.getArgumentAt(0, String.class);
- return accountMap.get(identifier);
+ return accountMap.get(identifier).account;
}
}
private static class CreateAccountAnswer implements Answer {
@Override
- public Void answer(InvocationOnMock invocation) throws Throwable {
+ public Void answer(final InvocationOnMock invocation) throws Throwable {
final Account account = invocation.getArgumentAt(0, Account.class);
- accountMap.put(account.getIdentifier(), account);
+ makeAccountResponsive(account, LocalDateTime.now(), (LedgerManager) invocation.getMock());
return null;
}
}
+ static class AccountEntriesStreamAnswer implements Answer {
+ private final AccountData accountData;
+
+ AccountEntriesStreamAnswer(final AccountData accountData) {
+ this.accountData = accountData;
+ }
+
+ @Override
+ public Stream<AccountEntry> answer(final InvocationOnMock invocation) throws Throwable {
+ return accountData.accountEntries.stream();
+ }
+ }
+
static void mockAccountingPrereqs(final LedgerManager ledgerManagerMock) {
- loanFundsSourceAccount();
- loanOriginationFeesIncomeAccount();
- processingFeeIncomeAccount();
- disbursementFeeIncomeAccount();
- tellerOneAccount();
- loanInterestAccrualAccount();
- consumerLoanInterestAccount();
+ makeAccountResponsive(loanFundsSourceAccount(), universalCreationDate, ledgerManagerMock);
+ makeAccountResponsive(loanOriginationFeesIncomeAccount(), universalCreationDate, ledgerManagerMock);
+ makeAccountResponsive(processingFeeIncomeAccount(), universalCreationDate, ledgerManagerMock);
+ makeAccountResponsive(disbursementFeeIncomeAccount(), universalCreationDate, ledgerManagerMock);
+ makeAccountResponsive(tellerOneAccount(), universalCreationDate, ledgerManagerMock);
+ makeAccountResponsive(loanInterestAccrualAccount(), universalCreationDate, ledgerManagerMock);
+ makeAccountResponsive(consumerLoanInterestAccount(), universalCreationDate, ledgerManagerMock);
Mockito.doReturn(incomeLedger()).when(ledgerManagerMock).findLedger(INCOME_LEDGER_IDENTIFIER);
Mockito.doReturn(feesAndChargesLedger()).when(ledgerManagerMock).findLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
@@ -355,6 +412,8 @@
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,
@@ -362,5 +421,8 @@
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/TestAccountingInteractionInLoanWorkflow.java b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
index ecf8a64..4d7a7b4 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -58,11 +58,13 @@
private BeatListener portfolioBeatListener;
- private static Product product;
- private static Case customerCase;
- private static CaseParameters caseParameters;
- private static String pendingDisbursalAccountIdentifier;
- private static String customerLoanAccountIdentifier;
+ private Product product = null;
+ private Case customerCase = null;
+ private CaseParameters caseParameters = null;
+ private String pendingDisbursalAccountIdentifier = null;
+ private String customerLoanAccountIdentifier = null;
+
+ private BigDecimal expectedCurrentBalance = null;
@Before
@@ -86,7 +88,7 @@
step4ApproveCase();
step5DisburseFullAmount();
step6CalculateInterestAccrual();
- //step7PaybackFullAmount();
+ step7PaybackFullAmount();
}
//Create product and set charges to fixed fees.
@@ -182,6 +184,8 @@
creditors.add(new Creditor(pendingDisbursalAccountIdentifier, caseParameters.getMaximumBalance().toPlainString()));
creditors.add(new Creditor(AccountingFixture.LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER, LOAN_ORIGINATION_FEE_AMOUNT.toPlainString()));
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+
+ expectedCurrentBalance = BigDecimal.ZERO;
}
//Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
@@ -207,6 +211,7 @@
creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, DISBURSEMENT_FEE_AMOUNT.toPlainString()));
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+ expectedCurrentBalance = expectedCurrentBalance.add(caseParameters.getMaximumBalance());
}
//Perform daily interest calculation.
@@ -215,7 +220,7 @@
final String beatIdentifier = "alignment0";
final String midnightTimeStamp = DateConverter.toIsoString(LocalDateTime.now().truncatedTo(ChronoUnit.DAYS));
- AccountingFixture.mockBalance(customerLoanAccountIdentifier, caseParameters.getMaximumBalance());
+ AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
final BeatPublish interestBeat = new BeatPublish(beatIdentifier, midnightTimeStamp);
portfolioBeatListener.publishBeat(interestBeat);
@@ -228,19 +233,44 @@
final Case customerCaseAfterStateChange = portfolioManager.getCase(product.getIdentifier(), customerCase.getIdentifier());
Assert.assertEquals(customerCaseAfterStateChange.getCurrentState(), Case.State.ACTIVE.name());
- final String 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)
- .toPlainString();
+ 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);
final Set<Debtor> debtors = new HashSet<>();
debtors.add(new Debtor(
AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT,
- calculatedInterest));
+ calculatedInterest.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
creditors.add(new Creditor(
customerLoanAccountIdentifier,
- calculatedInterest));
+ calculatedInterest.toPlainString()));
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+
+ expectedCurrentBalance = expectedCurrentBalance.add(calculatedInterest);
+ }
+
+ private void step7PaybackFullAmount() throws InterruptedException {
+ logger.info("step7PaybackFullAmount");
+
+ AccountingFixture.mockBalance(customerLoanAccountIdentifier, expectedCurrentBalance);
+
+ checkStateTransfer(
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ Action.ACCEPT_PAYMENT,
+ Collections.singletonList(assignEntryToTeller()),
+ IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
+ Case.State.CLOSED);
+ checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
+
+ final Set<Debtor> debtors = new HashSet<>();
+ debtors.add(new Debtor(customerLoanAccountIdentifier, expectedCurrentBalance.toPlainString()));
+
+ final Set<Creditor> creditors = new HashSet<>();
+ creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, expectedCurrentBalance.toPlainString()));
+ AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors);
+
+ expectedCurrentBalance = expectedCurrentBalance.subtract(expectedCurrentBalance);
}
}
\ No newline at end of file
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 96944a7..5e47c01 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
@@ -15,6 +15,8 @@
*/
package io.mifos.portfolio;
+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;
@@ -23,9 +25,13 @@
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.*;
@@ -70,6 +76,13 @@
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
+ final AccountEntry firstEntry = new AccountEntry();
+ firstEntry.setAmount(2000.0);
+ firstEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now()));
+ Mockito.doAnswer((x) -> Stream.of(firstEntry))
+ .when(ledgerManager)
+ .fetchAccountEntriesStream(Matchers.anyString(), Matchers.anyString(), Matchers.anyString());
+
checkStateTransfer(
product.getIdentifier(),
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestTaskDefinitions.java b/component-test/src/main/java/io/mifos/portfolio/TestTaskDefinitions.java
index 0472a75..25438b6 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestTaskDefinitions.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestTaskDefinitions.java
@@ -15,6 +15,8 @@
*/
package io.mifos.portfolio;
+import io.mifos.core.api.util.NotFoundException;
+import io.mifos.portfolio.api.v1.client.ProductInUseException;
import io.mifos.portfolio.api.v1.domain.Product;
import io.mifos.portfolio.api.v1.domain.TaskDefinition;
import io.mifos.portfolio.api.v1.events.EventConstants;
@@ -22,7 +24,6 @@
import org.junit.Assert;
import org.junit.Test;
-import java.util.HashSet;
import java.util.List;
/**
@@ -52,12 +53,29 @@
portfolioManager.changeTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier(), taskDefinition);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_TASK_DEFINITION,
- new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
+ new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
final TaskDefinition taskDefinitionRead = portfolioManager.getTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier());
Assert.assertEquals(taskDefinition,taskDefinitionRead);
}
+ @Test(expected = ProductInUseException.class)
+ public void shouldNotChangeTaskDefinitionForProductWithCases() throws InterruptedException {
+ final Product product = createProduct();
+ final TaskDefinition taskDefinition = createTaskDefinition(product);
+
+ enableProduct(product);
+
+ createCase(product.getIdentifier());
+
+ taskDefinition.setDescription("bleblablub");
+ taskDefinition.setFourEyes(false);
+
+ portfolioManager.changeTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier(), taskDefinition);
+ Assert.assertFalse(this.eventRecorder.wait(EventConstants.PUT_TASK_DEFINITION,
+ new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
+ }
+
@Test
public void shouldAddTaskDefinition() throws InterruptedException {
final Product product = createProduct();
@@ -71,21 +89,41 @@
Assert.assertTrue(tasks.size() == initialTaskCount + 1);
}
- private TaskDefinition createTaskDefinition(Product product) throws InterruptedException {
+ @Test(expected = ProductInUseException.class)
+ public void shouldNotCreateTaskDefinitionForProductWithCases() throws InterruptedException {
+ final Product product = createAndEnableProduct();
+ createCase(product.getIdentifier());
+
final TaskDefinition taskDefinition = getTaskDefinition();
portfolioManager.createTaskDefinition(product.getIdentifier(), taskDefinition);
- Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
- return taskDefinition;
+ Assert.assertFalse(this.eventRecorder.wait(EventConstants.POST_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
}
- private TaskDefinition getTaskDefinition() {
- final TaskDefinition ret = new TaskDefinition();
- ret.setIdentifier(Fixture.generateUniqueIdentifer("task"));
- ret.setDescription("But how do you feel about this?");
- ret.setName("feep");
- ret.setMandatory(false);
- ret.setActions(new HashSet<>());
- ret.setFourEyes(true);
- return ret;
+ @Test
+ public void shouldDeleteTaskDefinition() throws InterruptedException {
+ final Product product = createProduct();
+ final TaskDefinition taskDefinition = createTaskDefinition(product);
+
+ portfolioManager.deleteTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier());
+ Assert.assertTrue(this.eventRecorder.wait(EventConstants.DELETE_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
+
+ try {
+ portfolioManager.getTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier());
+ Assert.fail();
+ }
+ catch (final NotFoundException ignored) {
+ }
+ }
+
+ @Test(expected = ProductInUseException.class)
+ public void shouldNotDeleteTaskDefinitionForProductWithCases() throws InterruptedException {
+ final Product product = createProduct();
+ final TaskDefinition taskDefinition = createTaskDefinition(product);
+
+ enableProduct(product);
+ createCase(product.getIdentifier());
+
+ portfolioManager.deleteTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier());
+ Assert.assertFalse(this.eventRecorder.wait(EventConstants.DELETE_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier())));
}
}
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/portfolio/listener/TaskDefinitionEventListener.java b/component-test/src/main/java/io/mifos/portfolio/listener/TaskDefinitionEventListener.java
index 6865b99..adaf189 100644
--- a/component-test/src/main/java/io/mifos/portfolio/listener/TaskDefinitionEventListener.java
+++ b/component-test/src/main/java/io/mifos/portfolio/listener/TaskDefinitionEventListener.java
@@ -50,12 +50,22 @@
}
@JmsListener(
- subscription = EventConstants.DESTINATION,
- destination = EventConstants.DESTINATION,
- selector = EventConstants.SELECTOR_PUT_TASK_DEFINITION
+ subscription = EventConstants.DESTINATION,
+ destination = EventConstants.DESTINATION,
+ selector = EventConstants.SELECTOR_PUT_TASK_DEFINITION
)
public void onChangeTaskDefinition(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
- final String payload) {
+ final String payload) {
this.eventRecorder.event(tenant, EventConstants.PUT_TASK_DEFINITION, payload, TaskDefinitionEvent.class);
}
+
+ @JmsListener(
+ subscription = EventConstants.DESTINATION,
+ destination = EventConstants.DESTINATION,
+ selector = EventConstants.SELECTOR_DELETE_TASK_DEFINITION
+ )
+ public void onDeleteTaskDefinition(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+ final String payload) {
+ this.eventRecorder.event(tenant, EventConstants.DELETE_TASK_DEFINITION, payload, TaskDefinitionEvent.class);
+ }
}
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 f72ca48..549dc9b 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
@@ -23,6 +23,7 @@
import io.mifos.core.lang.ServiceException;
import io.mifos.individuallending.IndividualLendingPatternFactory;
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
@@ -41,11 +42,13 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
+import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -90,13 +93,17 @@
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
final List<ChargeInstance> charges = costComponents.stream()
- .map(x -> mapCostComponentEntryToChargeInstance(Action.OPEN, x, designatorToAccountIdentifierMapper))
+ .map(entry -> mapCostComponentEntryToChargeInstance(
+ Action.OPEN,
+ entry,
+ getRequestedChargeAmounts(command.getCommand().getCostComponents()),
+ designatorToAccountIdentifierMapper))
.collect(Collectors.toList());
accountingAdapter.bookCharges(charges,
- command.getCommand().getNote(),
- productIdentifier + "." + caseIdentifier + "." + Action.OPEN.name(),
- Action.OPEN.getTransactionType());
+ command.getCommand().getNote(),
+ dataContextOfAction.getMessageForCharge(Action.OPEN),
+ Action.OPEN.getTransactionType());
//Only move to new state if book charges command was accepted.
final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
customerCase.setCurrentState(Case.State.PENDING.name());
@@ -124,7 +131,11 @@
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
final List<ChargeInstance> charges = costComponents.stream()
- .map(x -> mapCostComponentEntryToChargeInstance(Action.DENY, x, designatorToAccountIdentifierMapper))
+ .map(entry -> mapCostComponentEntryToChargeInstance(
+ Action.DENY,
+ entry,
+ getRequestedChargeAmounts(command.getCommand().getCostComponents()),
+ designatorToAccountIdentifierMapper))
.collect(Collectors.toList());
final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
@@ -165,13 +176,17 @@
costComponentService.getCostComponentsForApprove(dataContextOfAction);
final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
- .map(x -> mapCostComponentEntryToChargeInstance(Action.APPROVE, x, designatorToAccountIdentifierMapper))
+ .map(entry -> mapCostComponentEntryToChargeInstance(
+ Action.APPROVE,
+ entry,
+ getRequestedChargeAmounts(command.getCommand().getCostComponents()),
+ designatorToAccountIdentifierMapper))
.collect(Collectors.toList());
accountingAdapter.bookCharges(charges,
- command.getCommand().getNote(),
- productIdentifier + "." + caseIdentifier + "." + Action.APPROVE.name(),
- Action.APPROVE.getTransactionType());
+ command.getCommand().getNote(),
+ dataContextOfAction.getMessageForCharge(Action.APPROVE),
+ Action.APPROVE.getTransactionType());
//Only move to new state if book charges command was accepted.
final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
@@ -200,13 +215,18 @@
final BigDecimal disbursalAmount = dataContextOfAction.getCaseParameters().getMaximumBalance();
final List<ChargeInstance> charges = Stream.concat(
- costComponentsForRepaymentPeriod.stream().map(x -> mapCostComponentEntryToChargeInstance(Action.DISBURSE, x, designatorToAccountIdentifierMapper)),
+ costComponentsForRepaymentPeriod.stream()
+ .map(entry -> mapCostComponentEntryToChargeInstance(
+ Action.DISBURSE,
+ entry,
+ getRequestedChargeAmounts(command.getCommand().getCostComponents()),
+ designatorToAccountIdentifierMapper)),
Stream.of(getDisbursalChargeInstance(disbursalAmount, designatorToAccountIdentifierMapper)))
.collect(Collectors.toList());
accountingAdapter.bookCharges(charges,
command.getCommand().getNote(),
- productIdentifier + "." + caseIdentifier + "." + Action.DISBURSE.name(),
+ dataContextOfAction.getMessageForCharge(Action.DISBURSE),
Action.DISBURSE.getTransactionType());
//Only move to new state if book charges command was accepted.
if (Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()) != Case.State.ACTIVE) {
@@ -244,12 +264,16 @@
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
- .map(x -> mapCostComponentEntryToChargeInstance(Action.APPLY_INTEREST, x, designatorToAccountIdentifierMapper))
+ .map(entry -> mapCostComponentEntryToChargeInstance(
+ Action.APPLY_INTEREST,
+ entry,
+ Collections.emptyMap(),
+ designatorToAccountIdentifierMapper))
.collect(Collectors.toList());
accountingAdapter.bookCharges(charges,
"",
- productIdentifier + "." + caseIdentifier + "." + Action.APPLY_INTEREST.name(),
+ dataContextOfAction.getMessageForCharge(Action.APPLY_INTEREST),
Action.APPLY_INTEREST.getTransactionType());
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier);
@@ -257,15 +281,57 @@
@Transactional
@CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
- @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE)
+ @EventEmitter(
+ selectorName = EventConstants.SELECTOR_NAME,
+ selectorValue = IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE)
public IndividualLoanCommandEvent process(final AcceptPaymentCommand 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, null);
IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.ACCEPT_PAYMENT);
- final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
- customerCase.setCurrentState(Case.State.ACTIVE.name());
- caseRepository.save(customerCase);
+ if (dataContextOfAction.getCustomerCase().getEndOfTerm() == null)
+ throw ServiceException.internalError(
+ "End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
+
+ final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
+ costComponentService.getCostComponentsForAcceptPayment(dataContextOfAction);
+
+ final Map<String, BigDecimal> requestedChargeAmounts
+ = getRequestedChargeAmounts(command.getCommand().getCostComponents());
+
+ final BigDecimal sumOfAdjustments = costComponentsForRepaymentPeriod.stream()
+ .filter(entry -> entry.getKey().getIdentifier().equals(ChargeIdentifiers.PAYMENT_ID))
+ .map(entry -> getChargeAmount(
+ requestedChargeAmounts.get(entry.getKey().getIdentifier()),
+ entry.getValue().getAmount()))
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+
+ final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
+ .map(entry -> mapCostComponentEntryToChargeInstance(
+ Action.ACCEPT_PAYMENT,
+ entry,
+ requestedChargeAmounts,
+ designatorToAccountIdentifierMapper))
+ .collect(Collectors.toList());
+
+
+ accountingAdapter.bookCharges(charges,
+ command.getCommand().getNote(),
+ 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());
}
@@ -311,29 +377,41 @@
return new IndividualLoanCommandEvent(command.getProductIdentifier(), command.getCaseIdentifier());
}
-
private static ChargeInstance mapCostComponentEntryToChargeInstance(
final Action action,
final Map.Entry<ChargeDefinition, CostComponent> costComponentEntry,
+ final Map<String, BigDecimal> requestedChargeAmounts,
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
final ChargeDefinition chargeDefinition = costComponentEntry.getKey();
+
+ final BigDecimal requestedChargeAmount = requestedChargeAmounts.get(chargeDefinition.getIdentifier());
+ final BigDecimal configuredChargeAmount = costComponentEntry.getValue().getAmount();
+
+ final BigDecimal finalChargeAmount = getChargeAmount(requestedChargeAmount, configuredChargeAmount);
+
if (chargeDefinition.getAccrualAccountDesignator() != null) {
if (Action.valueOf(chargeDefinition.getAccrueAction()) == action)
return new ChargeInstance(
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
- costComponentEntry.getValue().getAmount());
+ finalChargeAmount);
else
return new ChargeInstance(
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
- costComponentEntry.getValue().getAmount());
+ finalChargeAmount);
}
else
return new ChargeInstance(
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
- costComponentEntry.getValue().getAmount());
+ finalChargeAmount);
+ }
+
+ private static BigDecimal getChargeAmount(
+ final BigDecimal requestedChargeAmount,
+ final BigDecimal configuredChargeAmount) {
+ return requestedChargeAmount != null ? requestedChargeAmount : configuredChargeAmount;
}
private static ChargeInstance getDisbursalChargeInstance(
@@ -344,4 +422,16 @@
designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN),
amount);
}
+
+ private Map<String, BigDecimal> getRequestedChargeAmounts(final @Nullable List<CostComponent> costComponents) {
+ if (costComponents == null)
+ return Collections.emptyMap();
+ else
+ return costComponents.stream()
+ .collect(Collectors.groupingBy(
+ CostComponent::getChargeIdentifier,
+ Collectors.reducing(BigDecimal.ZERO,
+ CostComponent::getAmount,
+ BigDecimal::add)));
+ }
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java
index bccade4..573a405 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
@@ -36,6 +36,7 @@
import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.time.LocalDate;
+import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.function.BiFunction;
@@ -179,8 +180,9 @@
minorCurrencyUnitDigits);
}
- public CostComponentsForRepaymentPeriod getCostComponentsForApplyInterest(final DataContextOfAction dataContextOfAction) {
-
+ public CostComponentsForRepaymentPeriod getCostComponentsForApplyInterest(
+ final DataContextOfAction dataContextOfAction)
+ {
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
@@ -189,19 +191,80 @@
final CaseParameters caseParameters = dataContextOfAction.getCaseParameters();
final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProduct().getMinorCurrencyUnitDigits();
- final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getScheduledActionsForDisbursedLoan(LocalDate.now(), dataContextOfAction.getCustomerCase().getEndOfTerm().toLocalDate(), caseParameters, Action.APPLY_INTEREST);
- final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(productIdentifier, minorCurrencyUnitDigits, currentBalance, scheduledActions);
+ final LocalDate today = today();
+ final ScheduledAction interestAction = new ScheduledAction(Action.APPLY_INTEREST, today, new Period(1, today));
+
+ final List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ productIdentifier,
+ minorCurrencyUnitDigits,
+ currentBalance,
+ Collections.singletonList(interestAction));
return getCostComponentsForScheduledCharges(
- scheduledCharges,
- caseParameters.getMaximumBalance(),
- currentBalance,
- minorCurrencyUnitDigits);
+ scheduledCharges,
+ caseParameters.getMaximumBalance(),
+ currentBalance,
+ minorCurrencyUnitDigits);
}
- private CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(final DataContextOfAction dataContextOfAction) {
- return null;
+ public CostComponentsForRepaymentPeriod getCostComponentsForAcceptPayment(
+ final DataContextOfAction dataContextOfAction)
+ {
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ 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 List<ScheduledCharge> scheduledCharges = individualLoanService.getScheduledCharges(
+ productIdentifier,
+ minorCurrencyUnitDigits,
+ currentBalance,
+ scheduledActions);
+
+ return getCostComponentsForScheduledCharges(
+ scheduledCharges,
+ caseParameters.getMaximumBalance(),
+ currentBalance,
+ minorCurrencyUnitDigits);
}
+
+ private LocalDate getStartOfTermOrThrow(final DataContextOfAction dataContextOfAction,
+ final String customerLoanAccountIdentifier) {
+ final Optional<LocalDateTime> firstDisbursalDateTime = accountingAdapter.getDateOfOldestEntryContainingMessage(
+ customerLoanAccountIdentifier,
+ dataContextOfAction.getMessageForCharge(Action.DISBURSE));
+
+ return firstDisbursalDateTime.map(LocalDateTime::toLocalDate)
+ .orElseThrow(() -> ServiceException.internalError(
+ "Start of term for loan ''{0}'' could not be acquired from accounting.",
+ dataContextOfAction.getCompoundIdentifer()));
+ }
+
private CostComponentsForRepaymentPeriod getCostComponentsForMarkLate(final DataContextOfAction dataContextOfAction) {
return null;
}
@@ -243,6 +306,7 @@
}
return new CostComponentsForRepaymentPeriod(
+ runningBalance,
costComponentMap,
balanceAdjustment);
}
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
index a51e975..defa570 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java
@@ -26,17 +26,32 @@
* @author Myrle Krantz
*/
public class CostComponentsForRepaymentPeriod {
- final Map<ChargeDefinition, CostComponent> costComponents;
- final BigDecimal balanceAdjustment;
+ final private BigDecimal runningBalance;
+ final private Map<ChargeDefinition, CostComponent> costComponents;
+ final private BigDecimal balanceAdjustment;
CostComponentsForRepaymentPeriod(
- final Map<ChargeDefinition, CostComponent> costComponents,
- final BigDecimal balanceAdjustment) {
+ final BigDecimal runningBalance,
+ final Map<ChargeDefinition, CostComponent> costComponents,
+ final BigDecimal balanceAdjustment) {
+ this.runningBalance = runningBalance;
this.costComponents = costComponents;
this.balanceAdjustment = balanceAdjustment;
}
+ public BigDecimal getRunningBalance() {
+ return runningBalance;
+ }
+
+ Map<ChargeDefinition, CostComponent> getCostComponents() {
+ return costComponents;
+ }
+
public Stream<Map.Entry<ChargeDefinition, CostComponent>> stream() {
return costComponents.entrySet().stream();
}
+
+ BigDecimal getBalanceAdjustment() {
+ return balanceAdjustment;
+ }
}
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
index 962efb2..f7c2485 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/DataContextOfAction.java
@@ -16,6 +16,7 @@
package io.mifos.individuallending.internal.service;
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.AccountAssignment;
import io.mifos.portfolio.service.internal.repository.CaseEntity;
import io.mifos.portfolio.service.internal.repository.ProductEntity;
@@ -56,7 +57,15 @@
return caseParameters;
}
- public @Nonnull List<AccountAssignment> getOneTimeAccountAssignments() {
+ @Nonnull List<AccountAssignment> getOneTimeAccountAssignments() {
return oneTimeAccountAssignments;
}
+
+ String getCompoundIdentifer() {
+ return product.getIdentifier() + "." + customerCase.getIdentifier();
+ }
+
+ public String getMessageForCharge(final Action action) {
+ return getCompoundIdentifer() + "." + action.name();
+ }
}
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 e7288a2..5dcf51a 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
@@ -199,9 +199,9 @@
CostComponentService.getCostComponentsForScheduledCharges(scheduledChargesInPeriod, balance, balance, minorCurrencyUnitDigits);
final PlannedPayment plannedPayment = new PlannedPayment();
- plannedPayment.setCostComponents(new ArrayList<>(costComponentsForRepaymentPeriod.costComponents.values()));
+ plannedPayment.setCostComponents(new ArrayList<>(costComponentsForRepaymentPeriod.getCostComponents().values()));
plannedPayment.setDate(repaymentPeriod.getEndDateAsString());
- balance = balance.add(costComponentsForRepaymentPeriod.balanceAdjustment);
+ balance = balance.add(costComponentsForRepaymentPeriod.getBalanceAdjustment());
plannedPayment.setRemainingPrincipal(balance);
plannedPayments.add(plannedPayment);
}
@@ -234,9 +234,14 @@
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
final ChargeDefinition acceptPaymentDefinition) {
return scheduledActions.stream()
- .flatMap(scheduledAction -> getChargeDefinitionStream(chargeDefinitionsMappedByChargeAction, chargeDefinitionsMappedByAccrueAction, acceptPaymentDefinition, scheduledAction)
- .map(chargeDefinition -> new ScheduledCharge(scheduledAction, chargeDefinition)))
- .collect(Collectors.toList());
+ .flatMap(scheduledAction ->
+ getChargeDefinitionStream(
+ chargeDefinitionsMappedByChargeAction,
+ chargeDefinitionsMappedByAccrueAction,
+ acceptPaymentDefinition,
+ scheduledAction)
+ .map(chargeDefinition -> new ScheduledCharge(scheduledAction, chargeDefinition)))
+ .collect(Collectors.toList());
}
private Stream<ChargeDefinition> getChargeDefinitionStream(
@@ -244,20 +249,25 @@
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAccrueAction,
final ChargeDefinition acceptPaymentDefinition,
final ScheduledAction scheduledAction) {
- List<ChargeDefinition> chargeMapping = chargeDefinitionsMappedByChargeAction.get(scheduledAction.action.name());
- if ((chargeMapping == null) && (scheduledAction.action == Action.valueOf(acceptPaymentDefinition.getChargeAction())))
- chargeMapping = Collections.singletonList(acceptPaymentDefinition);
-
+ final List<ChargeDefinition> chargeMappingList = chargeDefinitionsMappedByChargeAction
+ .get(scheduledAction.action.name());
+ Stream<ChargeDefinition> chargeMapping = chargeMappingList == null ? Stream.empty() : chargeMappingList.stream();
if (chargeMapping == null)
- chargeMapping = Collections.emptyList();
+ chargeMapping = Stream.empty();
- List<ChargeDefinition> accrueMapping = chargeDefinitionsMappedByAccrueAction.get(scheduledAction.action.name());
- if ((accrueMapping == null) && (scheduledAction.action == Action.valueOf(acceptPaymentDefinition.getChargeAction())))
- accrueMapping = Collections.singletonList(acceptPaymentDefinition);
+ 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 = Collections.emptyList();
+ accrueMapping = Stream.empty();
- return Stream.concat(accrueMapping.stream(), chargeMapping.stream());
+ if ((acceptPaymentDefinition.getAccrueAction() != null) && (scheduledAction.action == Action.valueOf(acceptPaymentDefinition.getAccrueAction())))
+ accrueMapping = Stream.concat(chargeMapping, Stream.of(acceptPaymentDefinition));
+
+
+ return Stream.concat(accrueMapping, chargeMapping);
}
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/Period.java b/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
index ecd04f2..7718553 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/Period.java
@@ -40,6 +40,11 @@
this.endDate = beginDate.plusDays(periodLength);
}
+ Period(final int periodLength, final LocalDate endDate) {
+ this.beginDate = endDate.minusDays(periodLength);
+ this.endDate = endDate;
+ }
+
LocalDate getBeginDate() {
return beginDate;
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
index 3466c21..d6186a7 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/PeriodChargeCalculator.java
@@ -44,7 +44,8 @@
private static boolean accruedCharge(final ScheduledCharge scheduledCharge)
{
return scheduledCharge.getChargeDefinition().getAccrualAccountDesignator() != null &&
- scheduledCharge.getChargeDefinition().getAccrueAction() != null;
+ scheduledCharge.getChargeDefinition().getAccrueAction() != null &&
+ scheduledCharge.getScheduledAction().repaymentPeriod != null;
}
static BigDecimal chargeAmountPerPeriod(final ScheduledCharge scheduledCharge, final int precision)
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 30c77fe..1d85ecb 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
@@ -42,6 +42,15 @@
}
ScheduledAction(@Nonnull final Action action,
+ @Nonnull final LocalDate when,
+ @Nonnull final Period actionPeriod) {
+ this.action = action;
+ this.when = when;
+ this.actionPeriod = actionPeriod;
+ this.repaymentPeriod = null;
+ }
+
+ ScheduledAction(@Nonnull final Action action,
@Nonnull final LocalDate when) {
this.action = action;
this.when = when;
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 80dcf48..e310af1 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
@@ -15,9 +15,9 @@
*/
package io.mifos.individuallending.internal.service;
-import io.mifos.portfolio.api.v1.domain.PaymentCycle;
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.PaymentCycle;
import javax.annotation.Nonnull;
import java.time.DayOfWeek;
@@ -25,9 +25,7 @@
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
-import java.util.List;
-import java.util.SortedSet;
-import java.util.TreeSet;
+import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -37,57 +35,61 @@
@SuppressWarnings("WeakerAccess")
public class ScheduledActionHelpers {
public static boolean actionHasNoActionPeriod(final Action action) {
- return preDisbursalActions().anyMatch(x -> action == x);
+ return preTermActions().anyMatch(x -> action == x) || postTermActions().anyMatch(x -> action == x);
}
- private static Stream<Action> preDisbursalActions() {
+ private static Stream<Action> preTermActions() {
return Stream.of(Action.OPEN, Action.APPROVE, Action.DISBURSE);
}
- public static List<ScheduledAction> getHypotheticalScheduledActions(final @Nonnull LocalDate initialDisbursalDate,
+ private static Stream<Action> postTermActions() {
+ return Stream.of(Action.CLOSE);
+ }
+
+ public static List<ScheduledAction> getHypotheticalScheduledActions(final @Nonnull LocalDate startOfTerm,
final @Nonnull CaseParameters caseParameters)
{
- final LocalDate endOfTerm = getRoughEndDate(initialDisbursalDate, caseParameters);
- return Stream.concat(preDisbursalActions().map(action -> new ScheduledAction(action, initialDisbursalDate)),
- getHypotheticalScheduledActionsForDisbursedLoan(initialDisbursalDate, endOfTerm, caseParameters))
+ final LocalDate endOfTerm = getRoughEndDate(startOfTerm, caseParameters);
+ return Stream.concat( Stream.concat(
+ preTermActions().map(action -> new ScheduledAction(action, startOfTerm)),
+ getHypotheticalScheduledActionsForDisbursedLoan(startOfTerm, endOfTerm, caseParameters)),
+ postTermActions().map(action -> new ScheduledAction(action, endOfTerm)))
.collect(Collectors.toList());
}
- public static List<ScheduledAction> getScheduledActionsForDisbursedLoan(final @Nonnull LocalDate forDate,
- final @Nonnull LocalDate endOfTerm,
- final @Nonnull CaseParameters caseParameters,
- final @Nonnull Action action) {
- if (preDisbursalActions().anyMatch(x -> action == x))
- throw new IllegalStateException("Should not be calling getScheduledActionsForDisbursedLoan with an action which occurs before disbursement.");
+ 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.");
- final LocalDate today = LocalDate.now(ZoneId.of("UTC"));
- return getHypotheticalScheduledActionsForDisbursedLoan(today, endOfTerm, caseParameters)
+ 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(forDate))
- .collect(Collectors.toList());
+ .filter(x -> x.actionPeriod != null && x.actionPeriod.containsDate(now))
+ .sorted(Comparator.comparing(x -> x.actionPeriod))
+ .findFirst();
}
private static Stream<ScheduledAction> getHypotheticalScheduledActionsForDisbursedLoan(
- final @Nonnull LocalDate initialDisbursalDate,
+ final @Nonnull LocalDate startOfTerm,
final @Nonnull LocalDate endOfTerm,
final @Nonnull CaseParameters caseParameters)
{
- final SortedSet<Period> repaymentPeriods = generateRepaymentPeriods(initialDisbursalDate, endOfTerm, caseParameters);
- final Period lastPeriod = repaymentPeriods.last();
-
- return Stream.concat(repaymentPeriods.stream().flatMap(ScheduledActionHelpers::generateScheduledActionsForRepaymentPeriod),
- Stream.of(new ScheduledAction(Action.CLOSE, lastPeriod.getEndDate(), lastPeriod, lastPeriod)));
+ return generateRepaymentPeriods(startOfTerm, endOfTerm, caseParameters)
+ .flatMap(ScheduledActionHelpers::generateScheduledActionsForRepaymentPeriod);
}
/** 'Rough' end date, because if the repayment period takes the last period after that end date, then the repayment
period will 'win'.*/
- public static LocalDate getRoughEndDate(final @Nonnull LocalDate initialDisbursalDate,
+ public static LocalDate getRoughEndDate(final @Nonnull LocalDate startOfTerm,
final @Nonnull CaseParameters caseParameters) {
final Integer maximumTermSize = caseParameters.getTermRange().getMaximum();
final ChronoUnit termUnit = caseParameters.getTermRange().getTemporalUnit();
- return initialDisbursalDate.plus(
+ return startOfTerm.plus(
maximumTermSize,
termUnit);
}
@@ -107,24 +109,24 @@
.limit(ChronoUnit.DAYS.between(repaymentPeriod.getBeginDate(), repaymentPeriod.getEndDate()));
}
- private static SortedSet<Period> generateRepaymentPeriods(
- final LocalDate initialDisbursalDate,
- final LocalDate endDate,
+ private static Stream<Period> generateRepaymentPeriods(
+ final LocalDate startOfTerm,
+ final LocalDate endOfTerm,
final CaseParameters caseParameters) {
- final SortedSet<Period> ret = new TreeSet<>();
- LocalDate lastPaymentDate = initialDisbursalDate;
- LocalDate nextPaymentDate = generateNextPaymentDate(caseParameters, initialDisbursalDate);
- while (nextPaymentDate.isBefore(endDate))
+ final List<Period> ret = new ArrayList<>();
+ LocalDate lastPaymentDate = startOfTerm;
+ LocalDate nextPaymentDate = generateNextPaymentDate(caseParameters, lastPaymentDate);
+ while (nextPaymentDate.isBefore(endOfTerm))
{
final Period period = new Period(lastPaymentDate, nextPaymentDate);
ret.add(period);
lastPaymentDate = nextPaymentDate;
- nextPaymentDate = generateNextPaymentDate(caseParameters, nextPaymentDate);
+ nextPaymentDate = generateNextPaymentDate(caseParameters, lastPaymentDate);
}
ret.add(new Period(lastPaymentDate, nextPaymentDate));
- return ret;
+ return ret.stream();
}
private static LocalDate generateNextPaymentDate(final CaseParameters caseParameters, final LocalDate lastPaymentDate) {
@@ -150,10 +152,10 @@
return alignPaymentDate(orientedPaymentDate, maximumAlignmentChronoUnit, paymentCycle);
}
- private static LocalDate incrementPaymentDate(LocalDate paymentDate, PaymentCycle paymentCycle) {
+ private static LocalDate incrementPaymentDate(final LocalDate paymentDate, final PaymentCycle paymentCycle) {
return paymentDate.plus(
- paymentCycle.getPeriod(),
- paymentCycle.getTemporalUnit());
+ paymentCycle.getPeriod(),
+ paymentCycle.getTemporalUnit());
}
private static LocalDate orientPaymentDate(final LocalDate paymentDate, final ChronoUnit maximumSpecifiedAlignmentChronoUnit, PaymentCycle paymentCycle) {
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/DeleteTaskDefinitionCommand.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/DeleteTaskDefinitionCommand.java
new file mode 100644
index 0000000..8fdba76
--- /dev/null
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/DeleteTaskDefinitionCommand.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.portfolio.service.internal.command;
+
+/**
+ * @author Myrle Krantz
+ */
+public class DeleteTaskDefinitionCommand {
+ private final String productIdentifier;
+ private final String taskIdentifier;
+
+ public DeleteTaskDefinitionCommand(final String productIdentifier, final String taskIdentifier) {
+ this.productIdentifier = productIdentifier;
+ this.taskIdentifier = taskIdentifier;
+ }
+
+ public String getProductIdentifier() {
+ return productIdentifier;
+ }
+
+ public String getTaskIdentifier() {
+ return taskIdentifier;
+ }
+
+ @Override
+ public String toString() {
+ return "DeleteTaskDefinitionCommand{" +
+ "productIdentifier='" + productIdentifier + '\'' +
+ ", taskIdentifier='" + taskIdentifier + '\'' +
+ '}';
+ }
+}
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskDefinitionCommandHandler.java b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskDefinitionCommandHandler.java
index 039ba68..6f672d2 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskDefinitionCommandHandler.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/command/handler/TaskDefinitionCommandHandler.java
@@ -21,6 +21,7 @@
import io.mifos.portfolio.api.v1.events.TaskDefinitionEvent;
import io.mifos.portfolio.service.internal.command.ChangeTaskDefinitionCommand;
import io.mifos.portfolio.service.internal.command.CreateTaskDefinitionCommand;
+import io.mifos.portfolio.service.internal.command.DeleteTaskDefinitionCommand;
import io.mifos.portfolio.service.internal.mapper.TaskDefinitionMapper;
import io.mifos.portfolio.service.internal.repository.ProductEntity;
import io.mifos.portfolio.service.internal.repository.ProductRepository;
@@ -75,18 +76,33 @@
final String productIdentifier = changeTaskDefinitionCommand.getProductIdentifier();
final TaskDefinitionEntity existingTaskDefinition
- = taskDefinitionRepository.findByProductIdAndTaskIdentifier(productIdentifier, taskDefinition.getIdentifier())
- .orElseThrow(() -> ServiceException.internalError("task definition not found."));
+ = taskDefinitionRepository.findByProductIdAndTaskIdentifier(productIdentifier, taskDefinition.getIdentifier())
+ .orElseThrow(() -> ServiceException.internalError("task definition not found."));
final TaskDefinitionEntity taskDefinitionEntity =
- TaskDefinitionMapper.map(existingTaskDefinition.getProduct(), taskDefinition);
+ TaskDefinitionMapper.map(existingTaskDefinition.getProduct(), taskDefinition);
taskDefinitionEntity.setId(existingTaskDefinition.getId());
taskDefinitionEntity.setId(existingTaskDefinition.getId());
taskDefinitionRepository.save(taskDefinitionEntity);
return new TaskDefinitionEvent(
- changeTaskDefinitionCommand.getProductIdentifier(),
- changeTaskDefinitionCommand.getInstance().getIdentifier());
+ changeTaskDefinitionCommand.getProductIdentifier(),
+ changeTaskDefinitionCommand.getInstance().getIdentifier());
+ }
+
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ @EventEmitter(selectorName = EventConstants.SELECTOR_NAME, selectorValue = EventConstants.DELETE_TASK_DEFINITION)
+ public TaskDefinitionEvent process(final DeleteTaskDefinitionCommand deleteTaskDefinitionCommand) {
+ final String productIdentifier = deleteTaskDefinitionCommand.getProductIdentifier();
+ final String taskIdentifier = deleteTaskDefinitionCommand.getTaskIdentifier();
+
+ final TaskDefinitionEntity existingTaskDefinition
+ = taskDefinitionRepository.findByProductIdAndTaskIdentifier(productIdentifier, taskIdentifier)
+ .orElseThrow(() -> ServiceException.notFound("Task definition ''{0}.{1}'' not found.", productIdentifier, taskIdentifier));
+
+ taskDefinitionRepository.delete(existingTaskDefinition);
+
+ return new TaskDefinitionEvent(productIdentifier, taskIdentifier);
}
}
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 755640f..d504b23 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
@@ -21,6 +21,7 @@
import io.mifos.accounting.api.v1.domain.*;
import io.mifos.core.api.util.UserContextHolder;
import io.mifos.core.lang.DateConverter;
+import io.mifos.core.lang.DateRange;
import io.mifos.core.lang.ServiceException;
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
@@ -29,7 +30,9 @@
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
+import java.time.LocalDate;
import java.time.LocalDateTime;
+import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -41,7 +44,6 @@
*/
@Component
public class AccountingAdapter {
-
public enum IdentifierType {LEDGER, ACCOUNT}
private final LedgerManager ledgerManager;
@@ -79,6 +81,26 @@
ledgerManager.createJournalEntry(journalEntry);
}
+ public Optional<LocalDateTime> getDateOfOldestEntryContainingMessage(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)
+ .findFirst()
+ .map(AccountEntry::getTransactionDate)
+ .map(DateConverter::fromIsoString);
+ }
+
+ public BigDecimal sumMatchingEntriesSinceDate(final String accountIdentifier, final LocalDate startDate, final String message)
+ {
+ final DateRange fromLastPaymentUntilNow = oneSidedDateRange(startDate);
+ return ledgerManager.fetchAccountEntriesStream(accountIdentifier, fromLastPaymentUntilNow.toString(), message)
+ .map(AccountEntry::getAmount)
+ .map(BigDecimal::valueOf).reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
private static Optional<Debtor> mapToDebtor(final ChargeInstance chargeInstance) {
if (chargeInstance.getAmount().compareTo(BigDecimal.ZERO) == 0)
return Optional.empty();
@@ -187,4 +209,8 @@
else
return false;
}
+
+ private static DateRange oneSidedDateRange(final LocalDate start) {
+ return new DateRange(start, LocalDate.now(ZoneId.of("UTC")));
+ }
}
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/TaskDefinitionRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/TaskDefinitionRestController.java
index c524cbc..18c5439 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/TaskDefinitionRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/TaskDefinitionRestController.java
@@ -17,14 +17,16 @@
import io.mifos.anubis.annotation.AcceptedTokenType;
import io.mifos.anubis.annotation.Permittable;
+import io.mifos.core.command.gateway.CommandGateway;
+import io.mifos.core.lang.ServiceException;
import io.mifos.portfolio.api.v1.PermittableGroupIds;
import io.mifos.portfolio.api.v1.domain.TaskDefinition;
import io.mifos.portfolio.service.internal.command.ChangeTaskDefinitionCommand;
import io.mifos.portfolio.service.internal.command.CreateTaskDefinitionCommand;
+import io.mifos.portfolio.service.internal.command.DeleteTaskDefinitionCommand;
+import io.mifos.portfolio.service.internal.service.CaseService;
import io.mifos.portfolio.service.internal.service.ProductService;
import io.mifos.portfolio.service.internal.service.TaskDefinitionService;
-import io.mifos.core.command.gateway.CommandGateway;
-import io.mifos.core.lang.ServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -44,17 +46,20 @@
private final CommandGateway commandGateway;
private final TaskDefinitionService taskDefinitionService;
private final ProductService productService;
+ private final CaseService caseService;
@Autowired
public TaskDefinitionRestController(
- final CommandGateway commandGateway,
- final TaskDefinitionService taskDefinitionService,
- final ProductService productService)
+ final CommandGateway commandGateway,
+ final TaskDefinitionService taskDefinitionService,
+ final ProductService productService,
+ final CaseService caseService)
{
super();
this.commandGateway = commandGateway;
this.taskDefinitionService = taskDefinitionService;
this.productService = productService;
+ this.caseService = caseService;
}
@Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
@@ -73,9 +78,9 @@
@Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
@RequestMapping(
- method = RequestMethod.POST,
- consumes = MediaType.APPLICATION_JSON_VALUE,
- produces = MediaType.APPLICATION_JSON_VALUE
+ method = RequestMethod.POST,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
)
public @ResponseBody
ResponseEntity<Void> createTaskDefinition(
@@ -84,6 +89,8 @@
{
checkProductExists(productIdentifier);
+ checkProductChangeable(productIdentifier);
+
taskDefinitionService.findByIdentifier(productIdentifier, instance.getIdentifier())
.ifPresent(taskDefinition -> {throw ServiceException.conflict("Duplicate identifier: " + taskDefinition.getIdentifier());});
@@ -110,17 +117,19 @@
@Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
@RequestMapping(
- value = "{taskdefinitionidentifier}",
- method = RequestMethod.PUT,
- consumes = MediaType.APPLICATION_JSON_VALUE,
- produces = MediaType.ALL_VALUE
+ value = "{taskdefinitionidentifier}",
+ method = RequestMethod.PUT,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
)
- public ResponseEntity<Void> changeTaskDefinition(
+ public @ResponseBody ResponseEntity<Void> changeTaskDefinition(
@PathVariable("productidentifier") final String productIdentifier,
@PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier,
@RequestBody @Valid final TaskDefinition instance)
{
- checkProductExists(productIdentifier);
+ checkTaskDefinitionExists(productIdentifier, taskDefinitionIdentifier);
+
+ checkProductChangeable(productIdentifier);
if (!taskDefinitionIdentifier.equals(instance.getIdentifier()))
throw ServiceException.badRequest("Instance identifiers may not be changed.");
@@ -130,8 +139,40 @@
return ResponseEntity.accepted().build();
}
- private void checkProductExists(@PathVariable("productidentifier") String productIdentifier) {
+ @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.PRODUCT_MANAGEMENT)
+ @RequestMapping(
+ value = "/{taskdefinitionidentifier}",
+ method = RequestMethod.DELETE,
+ consumes = MediaType.ALL_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE
+ )
+ public @ResponseBody ResponseEntity<Void> deleteTaskDefinition(
+ @PathVariable("productidentifier") final String productIdentifier,
+ @PathVariable("taskdefinitionidentifier") final String taskDefinitionIdentifier
+ )
+ {
+ checkTaskDefinitionExists(productIdentifier, taskDefinitionIdentifier);
+
+ checkProductChangeable(productIdentifier);
+
+ commandGateway.process(new DeleteTaskDefinitionCommand(productIdentifier, taskDefinitionIdentifier));
+
+ return ResponseEntity.accepted().build();
+ }
+
+ private void checkTaskDefinitionExists(final String productIdentifier,
+ final String taskDefinitionIdentifier) {
+ taskDefinitionService.findByIdentifier(productIdentifier, taskDefinitionIdentifier)
+ .orElseThrow(() -> ServiceException.notFound("No task with the identifier ''{0}.{1}'' exists.", productIdentifier, taskDefinitionIdentifier));
+ }
+
+ private void checkProductExists(final String productIdentifier) {
productService.findByIdentifier(productIdentifier)
.orElseThrow(() -> ServiceException.notFound("Invalid product referenced."));
}
+
+ private void checkProductChangeable(final String productIdentifier) {
+ if (caseService.existsByProductIdentifier(productIdentifier))
+ throw ServiceException.conflict("Cases exist for product with the identifier ''{0}''. Product cannot be changed.", productIdentifier);
+ }
}
\ No newline at end of file
diff --git a/service/src/main/resources/application.yml b/service/src/main/resources/application.yml
index 4493358..6688e4a 100644
--- a/service/src/main/resources/application.yml
+++ b/service/src/main/resources/application.yml
@@ -75,13 +75,4 @@
threadName: async-processor-
flyway:
- enabled: false
-
-system:
- publicKey:
- exponent: 65537
- modulus: 21188023007955682867939457181271038457216099278949187456460742046123672432355777599460689470319454021384777684967830053993002724303461144745107517305075315187397862430851722919529943465029389248042840364475999768651348557757734298942211509744303551097953258597691851996692366468761965138767429272032120029271744611798874201312092155969603381492096789028306859853929900848124928201000469425135976322303229632628092728624143573273277870884919055453251617011673264035045823652246768583219018126865521694880333238485410601803458379987829318615730229086183405850999386270584135805252231189505197494383178133769189765423639
-
-portfolio:
- bookInterestAsUser: interest_user
- bookInterestInTimeSlot: 0
\ No newline at end of file
+ enabled: false
\ 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 de3c107..f194269 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
@@ -26,7 +26,6 @@
import io.mifos.portfolio.api.v1.domain.*;
import io.mifos.portfolio.service.internal.service.ChargeDefinitionService;
import io.mifos.portfolio.service.internal.service.ProductService;
-import io.mifos.portfolio.service.internal.util.ChargeInstance;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -57,14 +56,6 @@
this.localDate = localDate;
}
- Action getAction() {
- return action;
- }
-
- LocalDate getLocalDate() {
- return localDate;
- }
-
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -97,7 +88,7 @@
private LocalDate initialDisbursementDate;
private Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction;
private Set<String> expectedChargeIdentifiers = new HashSet<>(Arrays.asList(ChargeIdentifiers.INTEREST_ID, ChargeIdentifiers.PAYMENT_ID));
- private Map<ActionDatePair, List<ChargeInstance>> chargeInstancesForActions = new HashMap<>();
+ 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
//doing carefully first.
@@ -138,8 +129,8 @@
TestCase expectChargeInstancesForActionDatePair(final Action action,
final LocalDate forDate,
- final List<ChargeInstance> chargeInstances) {
- this.chargeInstancesForActions.put(new ActionDatePair(action, forDate), chargeInstances);
+ final List<ChargeDefinition> chargeDefinitions) {
+ this.chargeDefinitionsForActions.put(new ActionDatePair(action, forDate), chargeDefinitions);
return this;
}
@@ -177,13 +168,14 @@
final Map<String, List<ChargeDefinition>> chargeDefinitionsMappedByAction = new HashMap<>();
chargeDefinitionsMappedByAction.put(Action.APPLY_INTEREST.name(), getInterestChargeDefinition(0.01, ChronoUnit.YEARS));
- chargeDefinitionsMappedByAction.put(Action.OPEN.name(),
- Collections.singletonList(
- getFixedSingleChargeDefinition(10.0, Action.OPEN, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME)));
+ 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(
- 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)));
+ loanOriginationFeeCharge,
+ loanFundsAllocationCharge));
return new TestCase("simpleCase")
.minorCurrencyUnitDigits(2)
@@ -193,22 +185,9 @@
.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))))
+ .expectChargeInstancesForActionDatePair(Action.OPEN, initialDisbursementDate, Collections.singletonList(processingFeeCharge))
.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)
- )));
+ Arrays.asList(loanOriginationFeeCharge, loanFundsAllocationCharge));
}
private static TestCase yearLoanTestCase()
@@ -224,7 +203,7 @@
chargeDefinitionsMappedByAction.put(Action.APPLY_INTEREST.name(), getInterestChargeDefinition(0.10, ChronoUnit.YEARS));
return new TestCase("yearLoanTestCase")
- .minorCurrencyUnitDigits(2)
+ .minorCurrencyUnitDigits(3)
.caseParameters(caseParameters)
.initialDisbursementDate(initialDisbursementDate)
.chargeDefinitionsMappedByAction(chargeDefinitionsMappedByAction);
@@ -381,6 +360,44 @@
Assert.assertEquals(testCase.expectedChargeIdentifiers, resultChargeIdentifiers);
}
+ @Test
+ 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()
+ .filter(scheduledCharge -> scheduledCharge.getScheduledAction().action == Action.APPLY_INTEREST)
+ .map(scheduledCharge -> scheduledCharge.getScheduledAction().when)
+ .collect(Collectors.toList());
+
+ final List<LocalDate> allTheDaysAfterTheInitialDisbursementDate
+ = Stream.iterate(testCase.initialDisbursementDate.plusDays(1), interestDay -> interestDay.plusDays(1))
+ .limit(interestCalculationDates.size())
+ .collect(Collectors.toList());
+
+ Assert.assertEquals(interestCalculationDates, allTheDaysAfterTheInitialDisbursementDate);
+
+ 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 Map<ActionDatePair, Set<ChargeDefinition>> searchableScheduledCharges = scheduledCharges.stream()
+ .collect(
+ Collectors.groupingBy(scheduledCharge ->
+ 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)));
+ }
+
private double percentDifference(final BigDecimal maxPayment, final BigDecimal minPayment) {
final BigDecimal difference = maxPayment.subtract(minPayment);
final BigDecimal percentDifference = difference.divide(maxPayment, 4, BigDecimal.ROUND_UP);