Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3587004

Browse files
committedSep 18, 2017
List<CostComponents> -> Payment object
Corresponding changes to PlannedPayment Changes to charges and required account assignments.
1 parent ed8bca8 commit 3587004

34 files changed

+937
-757
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2017 Kuelap, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.mifos.individuallending.api.v1.domain.caseinstance;
17+
18+
import java.math.BigDecimal;
19+
import java.util.Objects;
20+
21+
/**
22+
* @author Myrle Krantz
23+
*/
24+
public class Balance {
25+
private String accountDesignator;
26+
private BigDecimal amount;
27+
28+
public String getAccountDesignator() {
29+
return accountDesignator;
30+
}
31+
32+
public void setAccountDesignator(String accountDesignator) {
33+
this.accountDesignator = accountDesignator;
34+
}
35+
36+
public BigDecimal getAmount() {
37+
return amount;
38+
}
39+
40+
public void setAmount(BigDecimal amount) {
41+
this.amount = amount;
42+
}
43+
44+
@Override
45+
public boolean equals(Object o) {
46+
if (this == o) return true;
47+
if (o == null || getClass() != o.getClass()) return false;
48+
Balance balance = (Balance) o;
49+
return Objects.equals(accountDesignator, balance.accountDesignator) &&
50+
Objects.equals(amount, balance.amount);
51+
}
52+
53+
@Override
54+
public int hashCode() {
55+
return Objects.hash(accountDesignator, amount);
56+
}
57+
58+
@Override
59+
public String toString() {
60+
return "Balance{" +
61+
"accountDesignator='" + accountDesignator + '\'' +
62+
", amount=" + amount +
63+
'}';
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017 The Mifos Initiative.
2+
* Copyright 2017 Kuelap, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,87 +15,59 @@
1515
*/
1616
package io.mifos.individuallending.api.v1.domain.caseinstance;
1717

18-
import io.mifos.portfolio.api.v1.domain.CostComponent;
18+
import io.mifos.portfolio.api.v1.domain.Payment;
1919

20-
import javax.annotation.Nullable;
2120
import java.math.BigDecimal;
22-
import java.util.List;
21+
import java.util.Map;
2322
import java.util.Objects;
2423

2524
/**
2625
* @author Myrle Krantz
2726
*/
28-
@SuppressWarnings({"WeakerAccess", "unused"})
29-
public final class PlannedPayment {
30-
private Double interestRate;
31-
private List<CostComponent> costComponents;
32-
private BigDecimal remainingPrincipal;
33-
private @Nullable String date;
27+
public class PlannedPayment {
28+
private Payment payment;
29+
private Map<String, BigDecimal> balances;
3430

35-
public PlannedPayment() {
31+
public PlannedPayment(Payment payment, Map<String, BigDecimal> balances) {
32+
this.payment = payment;
33+
this.balances = balances;
3634
}
3735

38-
public PlannedPayment(Double interestRate, List<CostComponent> costComponents, BigDecimal remainingPrincipal) {
39-
this.interestRate = interestRate;
40-
this.costComponents = costComponents;
41-
this.remainingPrincipal = remainingPrincipal;
36+
public Payment getPayment() {
37+
return payment;
4238
}
4339

44-
public Double getInterestRate() {
45-
return interestRate;
40+
public void setPayment(Payment payment) {
41+
this.payment = payment;
4642
}
4743

48-
public void setInterestRate(Double interestRate) {
49-
this.interestRate = interestRate;
44+
public Map<String, BigDecimal> getBalances() {
45+
return balances;
5046
}
5147

52-
public List<CostComponent> getCostComponents() {
53-
return costComponents;
54-
}
55-
56-
public void setCostComponents(List<CostComponent> costComponents) {
57-
this.costComponents = costComponents;
58-
}
59-
60-
public BigDecimal getRemainingPrincipal() {
61-
return remainingPrincipal;
62-
}
63-
64-
public void setRemainingPrincipal(BigDecimal remainingPrincipal) {
65-
this.remainingPrincipal = remainingPrincipal;
66-
}
67-
68-
public String getDate() {
69-
return date;
70-
}
71-
72-
public void setDate(String date) {
73-
this.date = date;
48+
public void setBalances(Map<String, BigDecimal> balances) {
49+
this.balances = balances;
7450
}
7551

7652
@Override
7753
public boolean equals(Object o) {
7854
if (this == o) return true;
7955
if (o == null || getClass() != o.getClass()) return false;
8056
PlannedPayment that = (PlannedPayment) o;
81-
return Objects.equals(interestRate, that.interestRate) &&
82-
Objects.equals(costComponents, that.costComponents) &&
83-
Objects.equals(remainingPrincipal, that.remainingPrincipal) &&
84-
Objects.equals(date, that.date);
57+
return Objects.equals(payment, that.payment) &&
58+
Objects.equals(balances, that.balances);
8559
}
8660

8761
@Override
8862
public int hashCode() {
89-
return Objects.hash(interestRate, costComponents, remainingPrincipal, date);
63+
return Objects.hash(payment, balances);
9064
}
9165

9266
@Override
9367
public String toString() {
9468
return "PlannedPayment{" +
95-
"interestRate=" + interestRate +
96-
", costComponents=" + costComponents +
97-
", remainingPrincipal=" + remainingPrincipal +
98-
", date='" + date + '\'' +
99-
'}';
69+
"payment=" + payment +
70+
", balances=" + balances +
71+
'}';
10072
}
10173
}

‎api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java

-2
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@
2121
@SuppressWarnings("unused")
2222
public interface AccountDesignators {
2323
String CUSTOMER_LOAN = "customer-loan";
24-
String PENDING_DISBURSAL = "pending-disbursal";
2524
String LOAN_FUNDS_SOURCE = "loan-funds-source";
26-
String LOANS_PAYABLE = "loans-payable";
2725
String PROCESSING_FEE_INCOME = "processing-fee-income";
2826
String ORIGINATION_FEE_INCOME = "origination-fee-income";
2927
String DISBURSEMENT_FEE_INCOME = "disbursement-fee-income";

‎api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java

-8
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@
2222
*/
2323
@SuppressWarnings("unused")
2424
public interface ChargeIdentifiers {
25-
String LOAN_FUNDS_ALLOCATION_NAME = "Loan funds allocation";
26-
String LOAN_FUNDS_ALLOCATION_ID = "loan-funds-allocation";
27-
String RETURN_DISBURSEMENT_NAME = "Return disbursement";
28-
String RETURN_DISBURSEMENT_ID = "return-disbursement";
2925
String INTEREST_NAME = "Interest";
3026
String INTEREST_ID = "interest";
3127
String ALLOW_FOR_WRITE_OFF_NAME = "Allow for write-off";
@@ -36,16 +32,12 @@ public interface ChargeIdentifiers {
3632
String DISBURSEMENT_FEE_ID = "disbursement-fee";
3733
String DISBURSE_PAYMENT_NAME = "Disburse payment";
3834
String DISBURSE_PAYMENT_ID = "disburse-payment";
39-
String TRACK_DISBURSAL_PAYMENT_NAME = "Track disburse payment";
40-
String TRACK_DISBURSAL_PAYMENT_ID = "track-disburse-payment";
4135
String LOAN_ORIGINATION_FEE_NAME = "Loan origination fee";
4236
String LOAN_ORIGINATION_FEE_ID = "loan-origination-fee";
4337
String PROCESSING_FEE_NAME = "Processing fee";
4438
String PROCESSING_FEE_ID = "processing-fee";
4539
String REPAYMENT_NAME = "Repayment";
4640
String REPAYMENT_ID = "repayment";
47-
String TRACK_RETURN_PRINCIPAL_NAME = "Track return principal";
48-
String TRACK_RETURN_PRINCIPAL_ID = "track-return-principal";
4941

5042
static String nameToIdentifier(String name) {
5143
return name.toLowerCase(Locale.US).replace(" ", "-");

‎api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeProportionalDesignator.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ public enum ChargeProportionalDesignator {
2525
NOT_PROPORTIONAL("{notproportional}", 0),
2626
MAXIMUM_BALANCE_DESIGNATOR("{maximumbalance}", 1),
2727
RUNNING_BALANCE_DESIGNATOR("{runningbalance}", 2),
28-
PRINCIPAL_ADJUSTMENT_DESIGNATOR("{principaladjustment}", 3),
29-
REPAYMENT_DESIGNATOR("{repayment}", 4),
28+
REQUESTED_DISBURSEMENT_DESIGNATOR("{requesteddisbursement}", 3),
29+
REQUESTED_REPAYMENT_DESIGNATOR("{requestedrepayment}", 4),
30+
CONTRACTUAL_REPAYMENT_DESIGNATOR("{contractualrepayment}", 5),
3031
;
3132

3233
private final String value;

‎api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import io.mifos.core.api.annotation.ThrowsException;
1919
import io.mifos.core.api.util.CustomFeignClientsConfiguration;
20+
import io.mifos.portfolio.api.v1.domain.Payment;
2021
import io.mifos.portfolio.api.v1.domain.*;
2122
import io.mifos.portfolio.api.v1.validation.ValidSortColumn;
2223
import io.mifos.portfolio.api.v1.validation.ValidSortDirection;
@@ -345,7 +346,7 @@ Set<String> getActionsForCase(
345346
produces = MediaType.ALL_VALUE,
346347
consumes = MediaType.APPLICATION_JSON_VALUE
347348
)
348-
List<CostComponent> getCostComponentsForAction(
349+
Payment getCostComponentsForAction(
349350
@PathVariable("productidentifier") final String productIdentifier,
350351
@PathVariable("caseidentifier") final String caseIdentifier,
351352
@PathVariable("actionidentifier") final String actionIdentifier,
@@ -360,7 +361,7 @@ List<CostComponent> getCostComponentsForAction(
360361
produces = MediaType.ALL_VALUE,
361362
consumes = MediaType.APPLICATION_JSON_VALUE
362363
)
363-
List<CostComponent> getCostComponentsForAction(
364+
Payment getCostComponentsForAction(
364365
@PathVariable("productidentifier") final String productIdentifier,
365366
@PathVariable("caseidentifier") final String caseIdentifier,
366367
@PathVariable("actionidentifier") final String actionIdentifier,
@@ -375,7 +376,7 @@ List<CostComponent> getCostComponentsForAction(
375376
produces = MediaType.ALL_VALUE,
376377
consumes = MediaType.APPLICATION_JSON_VALUE
377378
)
378-
List<CostComponent> getCostComponentsForAction(
379+
Payment getCostComponentsForAction(
379380
@PathVariable("productidentifier") final String productIdentifier,
380381
@PathVariable("caseidentifier") final String caseIdentifier,
381382
@PathVariable("actionidentifier") final String actionIdentifier,
@@ -388,7 +389,7 @@ List<CostComponent> getCostComponentsForAction(
388389
produces = MediaType.ALL_VALUE,
389390
consumes = MediaType.APPLICATION_JSON_VALUE
390391
)
391-
List<CostComponent> getCostComponentsForAction(
392+
Payment getCostComponentsForAction(
392393
@PathVariable("productidentifier") final String productIdentifier,
393394
@PathVariable("caseidentifier") final String caseIdentifier,
394395
@PathVariable("actionidentifier") final String actionIdentifier);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2017 The Mifos Initiative.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.mifos.portfolio.api.v1.domain;
17+
18+
import javax.annotation.Nullable;
19+
import java.math.BigDecimal;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Objects;
23+
24+
/**
25+
* @author Myrle Krantz
26+
*/
27+
@SuppressWarnings({"WeakerAccess", "unused"})
28+
public final class Payment {
29+
private List<CostComponent> costComponents;
30+
private Map<String, BigDecimal> balanceAdjustments;
31+
private @Nullable String date;
32+
33+
public Payment() {
34+
}
35+
36+
public Payment(List<CostComponent> costComponents, Map<String, BigDecimal> balanceAdjustments) {
37+
this.costComponents = costComponents;
38+
this.balanceAdjustments = balanceAdjustments;
39+
}
40+
41+
public List<CostComponent> getCostComponents() {
42+
return costComponents;
43+
}
44+
45+
public void setCostComponents(List<CostComponent> costComponents) {
46+
this.costComponents = costComponents;
47+
}
48+
49+
public Map<String, BigDecimal> getBalanceAdjustments() {
50+
return balanceAdjustments;
51+
}
52+
53+
public void setBalanceAdjustments(Map<String, BigDecimal> balanceAdjustments) {
54+
this.balanceAdjustments = balanceAdjustments;
55+
}
56+
57+
public String getDate() {
58+
return date;
59+
}
60+
61+
public void setDate(String date) {
62+
this.date = date;
63+
}
64+
65+
@Override
66+
public boolean equals(Object o) {
67+
if (this == o) return true;
68+
if (o == null || getClass() != o.getClass()) return false;
69+
Payment that = (Payment) o;
70+
return Objects.equals(costComponents, that.costComponents) &&
71+
Objects.equals(balanceAdjustments, that.balanceAdjustments) &&
72+
Objects.equals(date, that.date);
73+
}
74+
75+
@Override
76+
public int hashCode() {
77+
return Objects.hash(costComponents, balanceAdjustments, date);
78+
}
79+
80+
@Override
81+
public String toString() {
82+
return "Payment{" +
83+
"costComponents=" + costComponents +
84+
", balanceAdjustments=" + balanceAdjustments +
85+
", date='" + date + '\'' +
86+
'}';
87+
}
88+
}

‎api/src/test/java/io/mifos/Fixture.java

-7
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,9 @@
3535
*/
3636
@SuppressWarnings("WeakerAccess")
3737
public class Fixture {
38-
static final String INCOME_LEDGER_IDENTIFIER = "1000";
39-
static final String FEES_AND_CHARGES_LEDGER_IDENTIFIER = "1300";
40-
static final String CASH_LEDGER_IDENTIFIER = "7300";
41-
static final String PENDING_DISBURSAL_LEDGER_IDENTIFIER = "7320";
42-
static final String CUSTOMER_LOAN_LEDGER_IDENTIFIER = "7353";
4338
static final String LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER = "7310";
4439
static final String LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER = "1310";
4540
static final String PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER = "1312";
46-
static final String TELLER_ONE_ACCOUNT_IDENTIFIER = "7352";
4741

4842
static int uniquenessSuffix = 0;
4943

@@ -62,7 +56,6 @@ static public Product getTestProduct() {
6256
product.setMinorCurrencyUnitDigits(2);
6357

6458
final Set<AccountAssignment> accountAssignments = new HashSet<>();
65-
accountAssignments.add(new AccountAssignment(PENDING_DISBURSAL, PENDING_DISBURSAL_LEDGER_IDENTIFIER));
6659
accountAssignments.add(new AccountAssignment(PROCESSING_FEE_INCOME, PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER));
6760
accountAssignments.add(new AccountAssignment(ORIGINATION_FEE_INCOME, LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER));
6861
accountAssignments.add(new AccountAssignment(DISBURSEMENT_FEE_INCOME, "001-004"));

‎component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -268,14 +268,14 @@ void checkCostComponentForActionCorrect(final String productIdentifier,
268268
final Set<String> accountDesignators,
269269
final BigDecimal amount,
270270
final CostComponent... expectedCostComponents) {
271-
final List<CostComponent> costComponents = portfolioManager.getCostComponentsForAction(
271+
final Payment payment = portfolioManager.getCostComponentsForAction(
272272
productIdentifier,
273273
customerCaseIdentifier,
274274
action.name(),
275275
accountDesignators,
276276
amount
277277
);
278-
final Set<CostComponent> setOfCostComponents = new HashSet<>(costComponents);
278+
final Set<CostComponent> setOfCostComponents = new HashSet<>(payment.getCostComponents());
279279
final Set<CostComponent> setOfExpectedCostComponents = Stream.of(expectedCostComponents)
280280
.filter(x -> x.getAmount().compareTo(BigDecimal.ZERO) != 0)
281281
.collect(Collectors.toSet());

‎component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java

-54
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ class AccountingFixture {
5050
private static final String FEES_AND_CHARGES_LEDGER_IDENTIFIER = "1300";
5151
private static final String ASSET_LEDGER_IDENTIFIER = "7000";
5252
private static final String CASH_LEDGER_IDENTIFIER = "7300";
53-
static final String PENDING_DISBURSAL_LEDGER_IDENTIFIER = "7320";
5453
static final String CUSTOMER_LOAN_LEDGER_IDENTIFIER = "7353";
5554
private static final String ACCRUED_INCOME_LEDGER_IDENTIFIER = "7800";
5655

@@ -61,7 +60,6 @@ class AccountingFixture {
6160
static final String TELLER_ONE_ACCOUNT_IDENTIFIER = "7352";
6261
static final String LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER = "7810";
6362
static final String CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER = "1103";
64-
static final String LOANS_PAYABLE_ACCOUNT_IDENTIFIER ="8690";
6563
static final String LATE_FEE_INCOME_ACCOUNT_IDENTIFIER = "1311";
6664
static final String LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER = "7840";
6765
static final String ARREARS_ALLOWANCE_ACCOUNT_IDENTIFIER = "3010";
@@ -129,15 +127,6 @@ private static Ledger feesAndChargesLedger() {
129127
return ret;
130128
}
131129

132-
private static Ledger pendingDisbursalLedger() {
133-
final Ledger ret = new Ledger();
134-
ret.setIdentifier(PENDING_DISBURSAL_LEDGER_IDENTIFIER);
135-
ret.setParentLedgerIdentifier(CASH_LEDGER_IDENTIFIER);
136-
ret.setType(AccountType.ASSET.name());
137-
ret.setCreatedOn(DateConverter.toIsoString(universalCreationDate));
138-
return ret;
139-
}
140-
141130
private static Ledger customerLoanLedger() {
142131
final Ledger ret = new Ledger();
143132
ret.setIdentifier(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
@@ -223,14 +212,6 @@ private static Account consumerLoanInterestAccount() {
223212
return ret;
224213
}
225214

226-
private static Account loansPayableAccount() {
227-
final Account ret = new Account();
228-
ret.setIdentifier(LOANS_PAYABLE_ACCOUNT_IDENTIFIER);
229-
//ret.setLedger(LOAN_INCOME_LEDGER_IDENTIFIER);
230-
ret.setType(AccountType.LIABILITY.name());
231-
return ret;
232-
}
233-
234215
private static Account lateFeeIncomeAccount() {
235216
final Account ret = new Account();
236217
ret.setIdentifier(LATE_FEE_INCOME_ACCOUNT_IDENTIFIER);
@@ -270,23 +251,6 @@ private static AccountPage customerLoanAccountsPage() {
270251
return ret;
271252
}
272253

273-
private static Object pendingDisbursalAccountsPage() {
274-
final Account pendingDisbursalAccount1 = new Account();
275-
pendingDisbursalAccount1.setIdentifier("pendingDisbursalAccount1");
276-
277-
final Account pendingDisbursalAccount2 = new Account();
278-
pendingDisbursalAccount2.setIdentifier("pendingDisbursalAccount2");
279-
280-
final Account pendingDisbursalAccount3 = new Account();
281-
pendingDisbursalAccount3.setIdentifier("pendingDisbursalAccount3");
282-
283-
final AccountPage ret = new AccountPage();
284-
ret.setTotalElements(3L);
285-
ret.setTotalPages(1);
286-
ret.setAccounts(Arrays.asList(pendingDisbursalAccount1, pendingDisbursalAccount2, pendingDisbursalAccount3));
287-
return ret;
288-
}
289-
290254
private static <T> Valid<T> isValid() {
291255
return new Valid<>();
292256
}
@@ -457,22 +421,18 @@ static void mockAccountingPrereqs(final LedgerManager ledgerManagerMock) {
457421
makeAccountResponsive(tellerOneAccount(), universalCreationDate, ledgerManagerMock);
458422
makeAccountResponsive(loanInterestAccrualAccount(), universalCreationDate, ledgerManagerMock);
459423
makeAccountResponsive(consumerLoanInterestAccount(), universalCreationDate, ledgerManagerMock);
460-
makeAccountResponsive(loansPayableAccount(), universalCreationDate, ledgerManagerMock);
461424
makeAccountResponsive(lateFeeIncomeAccount(), universalCreationDate, ledgerManagerMock);
462425
makeAccountResponsive(lateFeeAccrualAccount(), universalCreationDate, ledgerManagerMock);
463426
makeAccountResponsive(arrearsAllowanceAccount(), universalCreationDate, ledgerManagerMock);
464427

465428
Mockito.doReturn(incomeLedger()).when(ledgerManagerMock).findLedger(INCOME_LEDGER_IDENTIFIER);
466429
Mockito.doReturn(feesAndChargesLedger()).when(ledgerManagerMock).findLedger(FEES_AND_CHARGES_LEDGER_IDENTIFIER);
467430
Mockito.doReturn(cashLedger()).when(ledgerManagerMock).findLedger(CASH_LEDGER_IDENTIFIER);
468-
Mockito.doReturn(pendingDisbursalLedger()).when(ledgerManagerMock).findLedger(PENDING_DISBURSAL_LEDGER_IDENTIFIER);
469431
Mockito.doReturn(customerLoanLedger()).when(ledgerManagerMock).findLedger(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
470432
Mockito.doReturn(loanIncomeLedger()).when(ledgerManagerMock).findLedger(LOAN_INCOME_LEDGER_IDENTIFIER);
471433
Mockito.doReturn(accruedIncomeLedger()).when(ledgerManagerMock).findLedger(ACCRUED_INCOME_LEDGER_IDENTIFIER);
472434
Mockito.doReturn(customerLoanAccountsPage()).when(ledgerManagerMock).fetchAccountsOfLedger(Mockito.eq(CUSTOMER_LOAN_LEDGER_IDENTIFIER),
473435
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
474-
Mockito.doReturn(pendingDisbursalAccountsPage()).when(ledgerManagerMock).fetchAccountsOfLedger(Mockito.eq(PENDING_DISBURSAL_LEDGER_IDENTIFIER),
475-
Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
476436

477437
Mockito.doAnswer(new FindAccountAnswer()).when(ledgerManagerMock).findAccount(Matchers.anyString());
478438
Mockito.doAnswer(new CreateAccountAnswer()).when(ledgerManagerMock).createAccount(Matchers.any());
@@ -491,20 +451,6 @@ static String verifyAccountCreation(final LedgerManager ledgerManager,
491451
return specifiesCorrectAccount.getMatchedArgument().getIdentifier();
492452
}
493453

494-
static void verifyTransfer(final LedgerManager ledgerManager,
495-
final String fromAccountIdentifier,
496-
final String toAccountIdentifier,
497-
final BigDecimal amount,
498-
final String productIdentifier,
499-
final String caseIdentifier,
500-
final Action action) {
501-
final JournalEntryMatcher specifiesCorrectJournalEntry = new JournalEntryMatcher(
502-
Collections.singleton(new Debtor(fromAccountIdentifier, amount.toPlainString())),
503-
Collections.singleton(new Creditor(toAccountIdentifier, amount.toPlainString())),
504-
productIdentifier, caseIdentifier, action);
505-
Mockito.verify(ledgerManager).createJournalEntry(AdditionalMatchers.and(argThat(isValid()), argThat(specifiesCorrectJournalEntry)));
506-
}
507-
508454
static void verifyTransfer(final LedgerManager ledgerManager,
509455
final Set<Debtor> debtors,
510456
final Set<Creditor> creditors,

‎component-test/src/main/java/io/mifos/portfolio/Fixture.java

-5
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,11 @@ static public Product getTestProduct() {
6060
product.setMinorCurrencyUnitDigits(MINOR_CURRENCY_UNIT_DIGITS);
6161

6262
final Set<AccountAssignment> accountAssignments = new HashSet<>();
63-
final AccountAssignment pendingDisbursalAccountAssignment = new AccountAssignment();
64-
pendingDisbursalAccountAssignment.setDesignator(PENDING_DISBURSAL);
65-
pendingDisbursalAccountAssignment.setLedgerIdentifier(PENDING_DISBURSAL_LEDGER_IDENTIFIER);
66-
accountAssignments.add(pendingDisbursalAccountAssignment);
6763
accountAssignments.add(new AccountAssignment(PROCESSING_FEE_INCOME, PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER));
6864
accountAssignments.add(new AccountAssignment(ORIGINATION_FEE_INCOME, LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER));
6965
accountAssignments.add(new AccountAssignment(DISBURSEMENT_FEE_INCOME, DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER));
7066
accountAssignments.add(new AccountAssignment(INTEREST_INCOME, CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER));
7167
accountAssignments.add(new AccountAssignment(INTEREST_ACCRUAL, LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER));
72-
accountAssignments.add(new AccountAssignment(LOANS_PAYABLE, LOANS_PAYABLE_ACCOUNT_IDENTIFIER));
7368
accountAssignments.add(new AccountAssignment(LATE_FEE_INCOME, LATE_FEE_INCOME_ACCOUNT_IDENTIFIER));
7469
accountAssignments.add(new AccountAssignment(LATE_FEE_ACCRUAL, LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER));
7570
accountAssignments.add(new AccountAssignment(ARREARS_ALLOWANCE, ARREARS_ALLOWANCE_ACCOUNT_IDENTIFIER));

‎component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java

+39-61
Large diffs are not rendered by default.

‎component-test/src/main/java/io/mifos/portfolio/TestChargeDefinitions.java

-4
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,7 @@ public void shouldProvisionAndListChargeDefinitions() throws InterruptedExceptio
5757

5858
final Set<String> expectedReadOnlyChargeDefinitionIdentifiers = Stream.of(
5959
ChargeIdentifiers.ALLOW_FOR_WRITE_OFF_ID,
60-
ChargeIdentifiers.LOAN_FUNDS_ALLOCATION_ID,
61-
ChargeIdentifiers.RETURN_DISBURSEMENT_ID,
6260
ChargeIdentifiers.DISBURSE_PAYMENT_ID,
63-
ChargeIdentifiers.TRACK_DISBURSAL_PAYMENT_ID,
64-
ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID,
6561
ChargeIdentifiers.INTEREST_ID,
6662
ChargeIdentifiers.REPAYMENT_ID)
6763
.collect(Collectors.toSet());

‎component-test/src/main/java/io/mifos/portfolio/TestCommands.java

+8-80
Original file line numberDiff line numberDiff line change
@@ -15,100 +15,25 @@
1515
*/
1616
package io.mifos.portfolio;
1717

18-
import io.mifos.accounting.api.v1.domain.AccountEntry;
19-
import io.mifos.core.lang.DateConverter;
2018
import io.mifos.individuallending.api.v1.domain.workflow.Action;
2119
import io.mifos.portfolio.api.v1.domain.Case;
2220
import io.mifos.portfolio.api.v1.domain.Product;
2321
import org.junit.Test;
24-
import org.mockito.Matchers;
25-
import org.mockito.Mockito;
2622

23+
import java.math.BigDecimal;
24+
import java.time.Clock;
2725
import java.time.LocalDateTime;
2826
import java.util.Collections;
29-
import java.util.stream.Stream;
3027

3128
import static io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants.*;
3229

3330
/**
3431
* @author Myrle Krantz
3532
*/
3633
public class TestCommands extends AbstractPortfolioTest {
37-
@Test
38-
public void testHappyWorkflow() throws InterruptedException {
39-
final Product product = createAndEnableProduct();
40-
final Case customerCase = createCase(product.getIdentifier());
41-
42-
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN);
43-
44-
45-
checkStateTransfer(
46-
product.getIdentifier(),
47-
customerCase.getIdentifier(),
48-
Action.OPEN,
49-
Collections.singletonList(assignEntryToTeller()),
50-
OPEN_INDIVIDUALLOAN_CASE,
51-
Case.State.PENDING);
52-
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Action.DENY);
53-
54-
55-
checkStateTransfer(product.getIdentifier(),
56-
customerCase.getIdentifier(),
57-
Action.APPROVE,
58-
Collections.singletonList(assignEntryToTeller()),
59-
APPROVE_INDIVIDUALLOAN_CASE,
60-
Case.State.APPROVED);
61-
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Action.CLOSE);
62-
63-
64-
checkStateTransfer(
65-
product.getIdentifier(),
66-
customerCase.getIdentifier(),
67-
Action.DISBURSE,
68-
Collections.singletonList(assignEntryToTeller()),
69-
DISBURSE_INDIVIDUALLOAN_CASE,
70-
Case.State.ACTIVE);
71-
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
72-
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
73-
74-
final AccountEntry firstEntry = new AccountEntry();
75-
firstEntry.setAmount(2000.0);
76-
firstEntry.setTransactionDate(DateConverter.toIsoString(LocalDateTime.now()));
77-
Mockito.doAnswer((x) -> Stream.of(firstEntry))
78-
.when(ledgerManager)
79-
.fetchAccountEntriesStream(Matchers.anyString(), Matchers.anyString(), Matchers.anyString(), Matchers.eq("ASC"));
80-
81-
82-
checkStateTransfer(
83-
product.getIdentifier(),
84-
customerCase.getIdentifier(),
85-
Action.ACCEPT_PAYMENT,
86-
Collections.singletonList(assignEntryToTeller()),
87-
ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
88-
Case.State.ACTIVE);
89-
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
90-
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
91-
92-
93-
checkStateTransfer(
94-
product.getIdentifier(),
95-
customerCase.getIdentifier(),
96-
Action.ACCEPT_PAYMENT,
97-
Collections.singletonList(assignEntryToTeller()),
98-
ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
99-
Case.State.ACTIVE);
100-
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
101-
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
102-
103-
checkStateTransfer(
104-
product.getIdentifier(),
105-
customerCase.getIdentifier(),
106-
Action.CLOSE,
107-
Collections.singletonList(assignEntryToTeller()),
108-
CLOSE_INDIVIDUALLOAN_CASE,
109-
Case.State.CLOSED);
110-
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
111-
}
34+
// Happy case test deleted because the case is covered in more detail in
35+
// TestAccountingInteractionInLoanWorkflow.
36+
//public void testHappyWorkflow() throws InterruptedException
11237

11338
@Test
11439
public void testBadCustomerWorkflow() throws InterruptedException {
@@ -142,8 +67,11 @@ public void testBadCustomerWorkflow() throws InterruptedException {
14267
product.getIdentifier(),
14368
customerCase.getIdentifier(),
14469
Action.DISBURSE,
70+
LocalDateTime.now(Clock.systemUTC()),
14571
Collections.singletonList(assignEntryToTeller()),
72+
BigDecimal.valueOf(2000L),
14673
DISBURSE_INDIVIDUALLOAN_CASE,
74+
midnightToday(),
14775
Case.State.ACTIVE);
14876
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
14977
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);

‎component-test/src/main/java/io/mifos/portfolio/TestIndividualLoans.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package io.mifos.portfolio;
1717

1818
import com.google.gson.Gson;
19+
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
1920
import io.mifos.portfolio.api.v1.domain.Case;
2021
import io.mifos.portfolio.api.v1.domain.CasePage;
2122
import io.mifos.portfolio.api.v1.domain.Product;
@@ -88,8 +89,8 @@ public void shouldReturnSmallPaymentPlan() throws InterruptedException {
8889

8990
Assert.assertNotNull(paymentScheduleFirstPage);
9091
paymentScheduleFirstPage.getElements().forEach(x -> {
91-
x.getCostComponents().forEach(y -> Assert.assertEquals(product.getMinorCurrencyUnitDigits(), y.getAmount().scale()));
92-
Assert.assertEquals(product.getMinorCurrencyUnitDigits(), x.getRemainingPrincipal().scale());
92+
x.getPayment().getCostComponents().forEach(y -> Assert.assertEquals(product.getMinorCurrencyUnitDigits(), y.getAmount().scale()));
93+
Assert.assertEquals(product.getMinorCurrencyUnitDigits(), x.getBalances().get(AccountDesignators.CUSTOMER_LOAN).scale());
9394
});
9495
}
9596

‎service/build.gradle

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ dependencies {
4747
[group: 'org.hibernate', name: 'hibernate-validator', version: versions.validator],
4848
[group: 'org.javamoney.lib', name: 'javamoney-calc', version: versions.javamoneylib],
4949
[group: 'javax.money', name: 'money-api', version: '1.0.1'],
50-
[group: 'org.javamoney', name: 'moneta', version: '1.0.1']
50+
[group: 'org.javamoney', name: 'moneta', version: '1.0.1'],
51+
[group: 'net.jodah', name: 'expiringmap', version: versions.expiringmap],
5152
)
5253
}
5354

‎service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java

+53-73
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package io.mifos.individuallending;
1717

18-
import com.google.common.collect.Sets;
1918
import com.google.gson.Gson;
2019
import io.mifos.core.lang.ServiceException;
2120
import io.mifos.customer.api.v1.client.CustomerManager;
@@ -30,10 +29,8 @@
3029
import io.mifos.individuallending.internal.service.CostComponentService;
3130
import io.mifos.individuallending.internal.service.DataContextOfAction;
3231
import io.mifos.individuallending.internal.service.DataContextService;
33-
import io.mifos.portfolio.api.v1.domain.Case;
34-
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
35-
import io.mifos.portfolio.api.v1.domain.CostComponent;
36-
import io.mifos.portfolio.api.v1.domain.Pattern;
32+
import io.mifos.individuallending.internal.service.PaymentBuilder;
33+
import io.mifos.portfolio.api.v1.domain.*;
3734
import io.mifos.portfolio.service.ServiceConstants;
3835
import io.mifos.products.spi.PatternFactory;
3936
import io.mifos.products.spi.ProductCommandDispatcher;
@@ -47,7 +44,6 @@
4744
import java.time.temporal.ChronoUnit;
4845
import java.util.*;
4946
import java.util.stream.Collectors;
50-
import java.util.stream.Stream;
5147

5248
import static io.mifos.individuallending.api.v1.domain.product.AccountDesignators.*;
5349
import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
@@ -88,8 +84,8 @@ public Pattern pattern() {
8884

8985
final Set<String> individualLendingRequiredAccounts = new HashSet<>();
9086
individualLendingRequiredAccounts.add(CUSTOMER_LOAN);
91-
individualLendingRequiredAccounts.add(PENDING_DISBURSAL);
92-
individualLendingRequiredAccounts.add(LOAN_FUNDS_SOURCE);
87+
//TODO: fix in migration individualLendingRequiredAccounts.add(PENDING_DISBURSAL);
88+
//was String PENDING_DISBURSAL = "pending-disbursal";
9389
individualLendingRequiredAccounts.add(LOAN_FUNDS_SOURCE);
9490
individualLendingRequiredAccounts.add(PROCESSING_FEE_INCOME);
9591
individualLendingRequiredAccounts.add(ORIGINATION_FEE_INCOME);
@@ -111,60 +107,64 @@ public List<ChargeDefinition> charges() {
111107
public static List<ChargeDefinition> defaultIndividualLoanCharges() {
112108
final List<ChargeDefinition> ret = new ArrayList<>();
113109
final ChargeDefinition processingFee = charge(
114-
PROCESSING_FEE_NAME,
115-
Action.OPEN,
116-
BigDecimal.ONE,
117-
ENTRY,
118-
PROCESSING_FEE_INCOME);
110+
PROCESSING_FEE_NAME,
111+
Action.DISBURSE, //TODO: fix existing charges in migration
112+
BigDecimal.ONE,
113+
CUSTOMER_LOAN, //TODO: fix existing charges in migration
114+
PROCESSING_FEE_INCOME);
119115
processingFee.setReadOnly(false);
120116

121117
final ChargeDefinition loanOriginationFee = charge(
122-
LOAN_ORIGINATION_FEE_NAME,
123-
Action.APPROVE,
124-
BigDecimal.ONE,
125-
ENTRY,
126-
ORIGINATION_FEE_INCOME);
118+
LOAN_ORIGINATION_FEE_NAME,
119+
Action.DISBURSE, //TODO: fix existing charges in migration
120+
BigDecimal.ONE,
121+
CUSTOMER_LOAN, //TODO: fix existing charges in migration
122+
ORIGINATION_FEE_INCOME);
127123
loanOriginationFee.setReadOnly(false);
128124

129-
final ChargeDefinition loanFundsAllocation = charge(
125+
/*final ChargeDefinition loanFundsAllocation = charge(
130126
LOAN_FUNDS_ALLOCATION_ID,
131127
Action.APPROVE,
132128
BigDecimal.valueOf(100),
133129
LOAN_FUNDS_SOURCE,
134130
PENDING_DISBURSAL);
135-
loanFundsAllocation.setReadOnly(true);
131+
loanFundsAllocation.setReadOnly(true);*/
132+
//TODO: handle removing this extraneous charge in migration.
136133

137134
final ChargeDefinition disbursementFee = charge(
138-
DISBURSEMENT_FEE_NAME,
139-
Action.DISBURSE,
140-
BigDecimal.valueOf(0.1),
141-
ENTRY,
142-
DISBURSEMENT_FEE_INCOME);
135+
DISBURSEMENT_FEE_NAME,
136+
Action.DISBURSE,
137+
BigDecimal.valueOf(0.1),
138+
CUSTOMER_LOAN, //TODO: fix existing charges in migration
139+
DISBURSEMENT_FEE_INCOME);
140+
disbursementFee.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue()); //TODO: fix existing charges in migration
143141
disbursementFee.setReadOnly(false);
144142

145143
final ChargeDefinition disbursePayment = new ChargeDefinition();
146144
disbursePayment.setChargeAction(Action.DISBURSE.name());
147145
disbursePayment.setIdentifier(DISBURSE_PAYMENT_ID);
148146
disbursePayment.setName(DISBURSE_PAYMENT_NAME);
149147
disbursePayment.setDescription(DISBURSE_PAYMENT_NAME);
150-
disbursePayment.setFromAccountDesignator(LOANS_PAYABLE);
148+
disbursePayment.setFromAccountDesignator(CUSTOMER_LOAN); //TODO: fix existing charges in migration
151149
disbursePayment.setToAccountDesignator(ENTRY);
152-
disbursePayment.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
150+
disbursePayment.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
153151
disbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
154152
disbursePayment.setAmount(BigDecimal.valueOf(100));
155153
disbursePayment.setReadOnly(true);
156154

155+
/*
157156
final ChargeDefinition trackPrincipalDisbursePayment = new ChargeDefinition();
158157
trackPrincipalDisbursePayment.setChargeAction(Action.DISBURSE.name());
159158
trackPrincipalDisbursePayment.setIdentifier(TRACK_DISBURSAL_PAYMENT_ID);
160159
trackPrincipalDisbursePayment.setName(TRACK_DISBURSAL_PAYMENT_NAME);
161160
trackPrincipalDisbursePayment.setDescription(TRACK_DISBURSAL_PAYMENT_NAME);
162161
trackPrincipalDisbursePayment.setFromAccountDesignator(PENDING_DISBURSAL);
163162
trackPrincipalDisbursePayment.setToAccountDesignator(CUSTOMER_LOAN);
164-
trackPrincipalDisbursePayment.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
163+
trackPrincipalDisbursePayment.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
165164
trackPrincipalDisbursePayment.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
166165
trackPrincipalDisbursePayment.setAmount(BigDecimal.valueOf(100));
167-
trackPrincipalDisbursePayment.setReadOnly(true);
166+
trackPrincipalDisbursePayment.setReadOnly(true);*/
167+
//TODO: handle removing this extraneous charge in migration.
168168

169169
final ChargeDefinition lateFee = charge(
170170
LATE_FEE_NAME,
@@ -174,17 +174,17 @@ public static List<ChargeDefinition> defaultIndividualLoanCharges() {
174174
LATE_FEE_INCOME);
175175
lateFee.setAccrueAction(Action.MARK_LATE.name());
176176
lateFee.setAccrualAccountDesignator(LATE_FEE_ACCRUAL);
177-
lateFee.setProportionalTo(ChargeProportionalDesignator.REPAYMENT_DESIGNATOR.getValue());
177+
lateFee.setProportionalTo(ChargeProportionalDesignator.CONTRACTUAL_REPAYMENT_DESIGNATOR.getValue());
178178
lateFee.setChargeOnTop(true);
179179
lateFee.setReadOnly(false);
180180

181181
//TODO: Make multiple write off allowance charges.
182182
final ChargeDefinition writeOffAllowanceCharge = charge(
183-
ALLOW_FOR_WRITE_OFF_NAME,
184-
Action.MARK_LATE,
185-
BigDecimal.valueOf(30),
186-
PENDING_DISBURSAL,
187-
ARREARS_ALLOWANCE);
183+
ALLOW_FOR_WRITE_OFF_NAME,
184+
Action.MARK_LATE,
185+
BigDecimal.valueOf(30),
186+
LOAN_FUNDS_SOURCE, //TODO: this and previous value ("pending-disbursal") are not correct and will require migration.
187+
ARREARS_ALLOWANCE);
188188
writeOffAllowanceCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
189189
writeOffAllowanceCharge.setReadOnly(true);
190190

@@ -206,46 +206,48 @@ public static List<ChargeDefinition> defaultIndividualLoanCharges() {
206206
customerRepaymentCharge.setIdentifier(REPAYMENT_ID);
207207
customerRepaymentCharge.setName(REPAYMENT_NAME);
208208
customerRepaymentCharge.setDescription(REPAYMENT_NAME);
209-
customerRepaymentCharge.setFromAccountDesignator(CUSTOMER_LOAN);
210-
customerRepaymentCharge.setToAccountDesignator(ENTRY);
211-
customerRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REPAYMENT_DESIGNATOR.getValue());
209+
customerRepaymentCharge.setFromAccountDesignator(ENTRY); //TODO: fix existing charges in migration
210+
customerRepaymentCharge.setToAccountDesignator(CUSTOMER_LOAN); //TODO: fix existing charges in migration
211+
customerRepaymentCharge.setProportionalTo(ChargeProportionalDesignator.REQUESTED_REPAYMENT_DESIGNATOR.getValue());
212212
customerRepaymentCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
213213
customerRepaymentCharge.setAmount(BigDecimal.valueOf(100));
214214
customerRepaymentCharge.setReadOnly(true);
215215

216-
final ChargeDefinition trackReturnPrincipalCharge = new ChargeDefinition();
216+
/*final ChargeDefinition trackReturnPrincipalCharge = new ChargeDefinition();
217217
trackReturnPrincipalCharge.setChargeAction(Action.ACCEPT_PAYMENT.name());
218218
trackReturnPrincipalCharge.setIdentifier(TRACK_RETURN_PRINCIPAL_ID);
219219
trackReturnPrincipalCharge.setName(TRACK_RETURN_PRINCIPAL_NAME);
220220
trackReturnPrincipalCharge.setDescription(TRACK_RETURN_PRINCIPAL_NAME);
221221
trackReturnPrincipalCharge.setFromAccountDesignator(LOAN_FUNDS_SOURCE);
222222
trackReturnPrincipalCharge.setToAccountDesignator(LOANS_PAYABLE);
223-
trackReturnPrincipalCharge.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR.getValue());
223+
trackReturnPrincipalCharge.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
224224
trackReturnPrincipalCharge.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
225225
trackReturnPrincipalCharge.setAmount(BigDecimal.valueOf(100));
226-
trackReturnPrincipalCharge.setReadOnly(true);
226+
trackReturnPrincipalCharge.setReadOnly(true);*/
227+
//TODO: handle removing this extraneous charge in migration.
227228

228-
final ChargeDefinition disbursementReturnCharge = charge(
229+
/*final ChargeDefinition disbursementReturnCharge = charge(
229230
RETURN_DISBURSEMENT_NAME,
230231
Action.CLOSE,
231232
BigDecimal.valueOf(100),
232233
PENDING_DISBURSAL,
233234
LOAN_FUNDS_SOURCE);
234235
disbursementReturnCharge.setProportionalTo(ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue());
235-
disbursementReturnCharge.setReadOnly(true);
236+
disbursementReturnCharge.setReadOnly(true);*/
237+
//TODO: handle removing this extraneous charge in migration.
236238

237239
ret.add(processingFee);
238240
ret.add(loanOriginationFee);
239-
ret.add(loanFundsAllocation);
241+
//TODO: ret.add(loanFundsAllocation);
240242
ret.add(disbursementFee);
241243
ret.add(disbursePayment);
242-
ret.add(trackPrincipalDisbursePayment);
244+
//TODO: ret.add(trackPrincipalDisbursePayment);
243245
ret.add(lateFee);
244246
ret.add(writeOffAllowanceCharge);
245247
ret.add(interestCharge);
246248
ret.add(customerRepaymentCharge);
247-
ret.add(trackReturnPrincipalCharge);
248-
ret.add(disbursementReturnCharge);
249+
//TODO: ret.add(trackReturnPrincipalCharge);
250+
//TODO: ret.add(disbursementReturnCharge);
249251

250252
return ret;
251253
}
@@ -344,7 +346,7 @@ public Set<String> getNextActionsForState(final Case.State state) {
344346
}
345347

346348
@Override
347-
public List<CostComponent> getCostComponentsForAction(
349+
public Payment getCostComponentsForAction(
348350
final String productIdentifier,
349351
final String caseIdentifier,
350352
final String actionIdentifier,
@@ -356,35 +358,13 @@ public List<CostComponent> getCostComponentsForAction(
356358
final Case.State caseState = Case.State.valueOf(dataContextOfAction.getCustomerCaseEntity().getCurrentState());
357359
checkActionCanBeExecuted(caseState, action);
358360

359-
Stream<Map.Entry<ChargeDefinition, CostComponent>> costComponentStream = costComponentService.getCostComponentsForAction(
361+
final PaymentBuilder paymentBuilder = costComponentService.getCostComponentsForAction(
360362
action,
361363
dataContextOfAction,
362364
forPaymentSize,
363-
forDateTime.toLocalDate())
364-
.stream();
365-
366-
if (!forAccountDesignators.isEmpty()) {
367-
costComponentStream = costComponentStream
368-
.filter(costComponentEntry -> chargeReferencesAccountDesignators(costComponentEntry.getKey(), action, forAccountDesignators));
369-
}
370-
371-
return costComponentStream
372-
.map(costComponentEntry -> new CostComponent(costComponentEntry.getKey().getIdentifier(), costComponentEntry.getValue().getAmount()))
373-
.collect(Collectors.toList());
374-
}
365+
forDateTime.toLocalDate());
375366

376-
private boolean chargeReferencesAccountDesignators(
377-
final ChargeDefinition chargeDefinition,
378-
final Action action,
379-
final Set<String> forAccountDesignators) {
380-
final Set<String> accountsToCompare = Sets.newHashSet(
381-
chargeDefinition.getFromAccountDesignator(),
382-
chargeDefinition.getToAccountDesignator()
383-
);
384-
if (chargeDefinition.getAccrualAccountDesignator() != null)
385-
accountsToCompare.add(chargeDefinition.getAccrualAccountDesignator());
386-
387-
return !Sets.intersection(accountsToCompare, forAccountDesignators).isEmpty();
367+
return paymentBuilder.buildPayment(action, forAccountDesignators);
388368
}
389369

390370
public static void checkActionCanBeExecuted(final Case.State state, final Action action) {

‎service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java

+19-108
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import io.mifos.individuallending.internal.service.*;
3333
import io.mifos.portfolio.api.v1.domain.AccountAssignment;
3434
import io.mifos.portfolio.api.v1.domain.Case;
35-
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
3635
import io.mifos.portfolio.api.v1.domain.CostComponent;
3736
import io.mifos.portfolio.api.v1.events.EventConstants;
3837
import io.mifos.portfolio.service.internal.mapper.CaseMapper;
@@ -99,20 +98,13 @@ public IndividualLoanCommandEvent process(final OpenCommand command) {
9998

10099
checkIfTasksAreOutstanding(dataContextOfAction, Action.OPEN);
101100

102-
final CostComponentsForRepaymentPeriod costComponents
101+
final PaymentBuilder paymentBuilder
103102
= costComponentService.getCostComponentsForOpen(dataContextOfAction);
104103

105104
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
106105
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
107106

108-
final List<ChargeInstance> charges = costComponents.stream()
109-
.map(entry -> mapCostComponentEntryToChargeInstance(
110-
Action.OPEN,
111-
entry,
112-
designatorToAccountIdentifierMapper))
113-
.filter(Optional::isPresent)
114-
.map(Optional::get)
115-
.collect(Collectors.toList());
107+
final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.OPEN, designatorToAccountIdentifierMapper);
116108

117109
final LocalDateTime today = today();
118110

@@ -143,20 +135,13 @@ public IndividualLoanCommandEvent process(final DenyCommand command) {
143135

144136
checkIfTasksAreOutstanding(dataContextOfAction, Action.DENY);
145137

146-
final CostComponentsForRepaymentPeriod costComponents
138+
final PaymentBuilder paymentBuilder
147139
= costComponentService.getCostComponentsForDeny(dataContextOfAction);
148140

149141
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
150142
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
151143

152-
final List<ChargeInstance> charges = costComponents.stream()
153-
.map(entry -> mapCostComponentEntryToChargeInstance(
154-
Action.DENY,
155-
entry,
156-
designatorToAccountIdentifierMapper))
157-
.filter(Optional::isPresent)
158-
.map(Optional::get)
159-
.collect(Collectors.toList());
144+
final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.DENY, designatorToAccountIdentifierMapper);
160145

161146
final LocalDateTime today = today();
162147

@@ -195,17 +180,10 @@ public IndividualLoanCommandEvent process(final ApproveCommand command) {
195180
);
196181
caseRepository.save(dataContextOfAction.getCustomerCaseEntity());
197182

198-
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
183+
final PaymentBuilder paymentBuilder =
199184
costComponentService.getCostComponentsForApprove(dataContextOfAction);
200185

201-
final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
202-
.map(entry -> mapCostComponentEntryToChargeInstance(
203-
Action.APPROVE,
204-
entry,
205-
designatorToAccountIdentifierMapper))
206-
.filter(Optional::isPresent)
207-
.map(Optional::get)
208-
.collect(Collectors.toList());
186+
final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.APPROVE, designatorToAccountIdentifierMapper);
209187

210188
final LocalDateTime today = today();
211189

@@ -236,21 +214,13 @@ public IndividualLoanCommandEvent process(final DisburseCommand command) {
236214
checkIfTasksAreOutstanding(dataContextOfAction, Action.DISBURSE);
237215

238216
final BigDecimal disbursalAmount = Optional.ofNullable(command.getCommand().getPaymentSize()).orElse(BigDecimal.ZERO);
239-
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
217+
final PaymentBuilder paymentBuilder =
240218
costComponentService.getCostComponentsForDisburse(dataContextOfAction, disbursalAmount);
241219

242220
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
243221
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
244222

245-
final List<ChargeInstance> charges =
246-
costComponentsForRepaymentPeriod.stream()
247-
.map(entry -> mapCostComponentEntryToChargeInstance(
248-
Action.DISBURSE,
249-
entry,
250-
designatorToAccountIdentifierMapper))
251-
.filter(Optional::isPresent)
252-
.map(Optional::get)
253-
.collect(Collectors.toList());
223+
final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.DISBURSE, designatorToAccountIdentifierMapper);
254224

255225
final LocalDateTime today = today();
256226

@@ -272,7 +242,7 @@ public IndividualLoanCommandEvent process(final DisburseCommand command) {
272242
final String customerLoanAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN);
273243
final BigDecimal currentBalance = accountingAdapter.getCurrentBalance(customerLoanAccountIdentifier).negate();
274244

275-
final BigDecimal newLoanPaymentSize = costComponentService.getLoanPaymentSize(
245+
final BigDecimal newLoanPaymentSize = costComponentService.getLoanPaymentSizeForSingleDisbursement(
276246
currentBalance.add(disbursalAmount),
277247
dataContextOfAction);
278248

@@ -298,20 +268,13 @@ public IndividualLoanCommandEvent process(final ApplyInterestCommand command) {
298268
throw ServiceException.internalError(
299269
"End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
300270

301-
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
271+
final PaymentBuilder paymentBuilder =
302272
costComponentService.getCostComponentsForApplyInterest(dataContextOfAction);
303273

304274
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
305275
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
306276

307-
final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
308-
.map(entry -> mapCostComponentEntryToChargeInstance(
309-
Action.APPLY_INTEREST,
310-
entry,
311-
designatorToAccountIdentifierMapper))
312-
.filter(Optional::isPresent)
313-
.map(Optional::get)
314-
.collect(Collectors.toList());
277+
final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.APPLY_INTEREST, designatorToAccountIdentifierMapper);
315278

316279
accountingAdapter.bookCharges(charges,
317280
"Applied interest on " + command.getForTime(),
@@ -340,7 +303,7 @@ public IndividualLoanCommandEvent process(final AcceptPaymentCommand command) {
340303
throw ServiceException.internalError(
341304
"End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
342305

343-
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
306+
final PaymentBuilder paymentBuilder =
344307
costComponentService.getCostComponentsForAcceptPayment(
345308
dataContextOfAction,
346309
command.getCommand().getPaymentSize(),
@@ -349,14 +312,7 @@ public IndividualLoanCommandEvent process(final AcceptPaymentCommand command) {
349312
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
350313
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
351314

352-
final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
353-
.map(entry -> mapCostComponentEntryToChargeInstance(
354-
Action.ACCEPT_PAYMENT,
355-
entry,
356-
designatorToAccountIdentifierMapper))
357-
.filter(Optional::isPresent)
358-
.map(Optional::get)
359-
.collect(Collectors.toList());
315+
final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.ACCEPT_PAYMENT, designatorToAccountIdentifierMapper);
360316

361317
final LocalDateTime today = today();
362318

@@ -387,20 +343,13 @@ public IndividualLoanCommandEvent process(final MarkLateCommand command) {
387343
throw ServiceException.internalError(
388344
"End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
389345

390-
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
346+
final PaymentBuilder paymentBuilder =
391347
costComponentService.getCostComponentsForMarkLate(dataContextOfAction, DateConverter.fromIsoString(command.getForTime()));
392348

393349
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
394350
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
395351

396-
final List<ChargeInstance> charges = costComponentsForRepaymentPeriod.stream()
397-
.map(entry -> mapCostComponentEntryToChargeInstance(
398-
Action.MARK_LATE,
399-
entry,
400-
designatorToAccountIdentifierMapper))
401-
.filter(Optional::isPresent)
402-
.map(Optional::get)
403-
.collect(Collectors.toList());
352+
final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.MARK_LATE, designatorToAccountIdentifierMapper);
404353

405354
final LocalDateTime today = today();
406355

@@ -446,29 +395,21 @@ public IndividualLoanCommandEvent process(final CloseCommand command) {
446395

447396
checkIfTasksAreOutstanding(dataContextOfAction, Action.CLOSE);
448397

449-
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
398+
final PaymentBuilder paymentBuilder =
450399
costComponentService.getCostComponentsForClose(dataContextOfAction);
451400

452401
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
453402
= new DesignatorToAccountIdentifierMapper(dataContextOfAction);
454403

455-
final List<ChargeInstance> charges =
456-
costComponentsForRepaymentPeriod.stream()
457-
.map(entry -> mapCostComponentEntryToChargeInstance(
458-
Action.DISBURSE,
459-
entry,
460-
designatorToAccountIdentifierMapper))
461-
.filter(Optional::isPresent)
462-
.map(Optional::get)
463-
.collect(Collectors.toList());
404+
final List<ChargeInstance> charges = paymentBuilder.buildCharges(Action.CLOSE, designatorToAccountIdentifierMapper);
464405

465406
final LocalDateTime today = today();
466407

467408
accountingAdapter.bookCharges(charges,
468409
command.getCommand().getNote(),
469410
command.getCommand().getCreatedOn(),
470-
dataContextOfAction.getMessageForCharge(Action.DISBURSE),
471-
Action.DISBURSE.getTransactionType());
411+
dataContextOfAction.getMessageForCharge(Action.CLOSE),
412+
Action.CLOSE.getTransactionType());
472413

473414
final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
474415
customerCase.setCurrentState(Case.State.CLOSED.name());
@@ -498,36 +439,6 @@ public IndividualLoanCommandEvent process(final RecoverCommand command) {
498439
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
499440
}
500441

501-
private static Optional<ChargeInstance> mapCostComponentEntryToChargeInstance(
502-
final Action action,
503-
final Map.Entry<ChargeDefinition, CostComponent> costComponentEntry,
504-
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
505-
final ChargeDefinition chargeDefinition = costComponentEntry.getKey();
506-
final BigDecimal chargeAmount = costComponentEntry.getValue().getAmount();
507-
508-
if (CostComponentService.chargeIsAccrued(chargeDefinition)) {
509-
if (Action.valueOf(chargeDefinition.getAccrueAction()) == action)
510-
return Optional.of(new ChargeInstance(
511-
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
512-
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
513-
chargeAmount));
514-
else if (Action.valueOf(chargeDefinition.getChargeAction()) == action)
515-
return Optional.of(new ChargeInstance(
516-
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
517-
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
518-
chargeAmount));
519-
else
520-
return Optional.empty();
521-
}
522-
else if (Action.valueOf(chargeDefinition.getChargeAction()) == action)
523-
return Optional.of(new ChargeInstance(
524-
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
525-
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
526-
chargeAmount));
527-
else
528-
return Optional.empty();
529-
}
530-
531442
private Map<String, BigDecimal> getRequestedChargeAmounts(final @Nullable List<CostComponent> costComponents) {
532443
if (costComponents == null)
533444
return Collections.emptyMap();

‎service/src/main/java/io/mifos/individuallending/internal/service/CostComponentService.java

+126-129
Large diffs are not rendered by default.

‎service/src/main/java/io/mifos/individuallending/internal/service/CostComponentsForRepaymentPeriod.java

-51
This file was deleted.

‎service/src/main/java/io/mifos/individuallending/internal/service/DesignatorToAccountIdentifierMapper.java

+10-5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import javax.annotation.Nonnull;
2727
import java.util.List;
28+
import java.util.Optional;
2829
import java.util.Set;
2930
import java.util.stream.Stream;
3031

@@ -51,12 +52,16 @@ private Stream<AccountAssignment> fixedAccountAssignmentsAsStream() {
5152
productAccountAssignments.stream().map(ProductMapper::mapAccountAssignmentEntity));
5253
}
5354

54-
public String mapOrThrow(final @Nonnull String accountDesignator) {
55+
public Optional<String> map(final @Nonnull String accountDesignator) {
5556
return allAccountAssignmentsAsStream()
56-
.filter(x -> x.getDesignator().equals(accountDesignator))
57-
.findFirst()
58-
.map(AccountAssignment::getAccountIdentifier)
59-
.orElseThrow(() -> ServiceException.badRequest("A required account designator was not set ''{0}''.", accountDesignator));
57+
.filter(x -> x.getDesignator().equals(accountDesignator))
58+
.findFirst()
59+
.map(AccountAssignment::getAccountIdentifier);
60+
}
61+
62+
public String mapOrThrow(final @Nonnull String accountDesignator) {
63+
return map(accountDesignator).orElseThrow(() ->
64+
ServiceException.badRequest("A required account designator was not set ''{0}''.", accountDesignator));
6065
}
6166

6267
public Stream<AccountAssignment> getLedgersNeedingAccounts() {

‎service/src/main/java/io/mifos/individuallending/internal/service/IndividualLoanService.java

+23-20
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public PlannedPaymentPage getPlannedPaymentsPage(
5656
final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(dataContextOfAction.getProductEntity().getIdentifier(), scheduledActions);
5757

5858
final BigDecimal loanPaymentSize = CostComponentService.getLoanPaymentSize(
59+
dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum(),
5960
dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum(),
6061
dataContextOfAction.getInterest(),
6162
minorCurrencyUnitDigits,
@@ -123,41 +124,43 @@ static private List<PlannedPayment> getPlannedPaymentsElements(
123124
.sorted()
124125
.collect(Collector.of(ArrayList::new, List::add, (left, right) -> { left.addAll(right); return left; }));
125126

126-
BigDecimal balance = initialBalance.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
127+
final SimulatedRunningBalances balances = new SimulatedRunningBalances();
127128
final List<PlannedPayment> plannedPayments = new ArrayList<>();
128129
for (int i = 0; i < sortedRepaymentPeriods.size(); i++)
129130
{
130131
final Period repaymentPeriod = sortedRepaymentPeriods.get(i);
131-
final BigDecimal currentLoanPaymentSize;
132-
if (repaymentPeriod.isDefined()) {
133-
// last repayment period: Force the proposed payment to "overhang". Cost component calculation
134-
// corrects last loan payment downwards but not upwards.
135-
if (i == sortedRepaymentPeriods.size() - 1)
136-
currentLoanPaymentSize = loanPaymentSize.add(BigDecimal.valueOf(sortedRepaymentPeriods.size()));
137-
else
138-
currentLoanPaymentSize = loanPaymentSize;
132+
final BigDecimal requestedRepayment;
133+
final BigDecimal requestedDisbursal;
134+
if (i == 0)
135+
{ //First "period" is actually just the OPEN/APPROVE/DISBURSAL action set.
136+
requestedRepayment = BigDecimal.ZERO;
137+
requestedDisbursal = initialBalance.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
138+
}
139+
else if (i == sortedRepaymentPeriods.size() - 1)
140+
{ //Last repayment period: Fill the proposed payment out to the remaining balance of the loan.
141+
requestedRepayment = loanPaymentSize; //TODO: wrong doesn't include last period of interest.
142+
requestedDisbursal = BigDecimal.ZERO;
143+
}
144+
else {
145+
requestedRepayment = loanPaymentSize;
146+
requestedDisbursal = BigDecimal.ZERO;
139147
}
140-
else
141-
currentLoanPaymentSize = BigDecimal.ZERO;
142148

143149
final SortedSet<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
144-
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
150+
final PaymentBuilder paymentBuilder =
145151
CostComponentService.getCostComponentsForScheduledCharges(
146152
Collections.emptyMap(),
147153
scheduledChargesInPeriod,
148154
initialBalance,
149-
balance,
150-
currentLoanPaymentSize,
155+
balances,
156+
loanPaymentSize,
157+
requestedDisbursal,
158+
requestedRepayment,
151159
interest,
152160
minorCurrencyUnitDigits,
153161
false);
154162

155-
final PlannedPayment plannedPayment = new PlannedPayment();
156-
plannedPayment.setCostComponents(new ArrayList<>(costComponentsForRepaymentPeriod.getCostComponents().values()));
157-
plannedPayment.setDate(repaymentPeriod.getEndDateAsString());
158-
balance = balance.add(costComponentsForRepaymentPeriod.getBalanceAdjustment());
159-
plannedPayment.setRemainingPrincipal(balance);
160-
plannedPayments.add(plannedPayment);
163+
plannedPayments.add(paymentBuilder.accumulatePlannedPayment(balances));
161164
}
162165
return plannedPayments;
163166
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
* Copyright 2017 The Mifos Initiative.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.mifos.individuallending.internal.service;
17+
18+
import com.google.common.collect.Sets;
19+
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
20+
import io.mifos.individuallending.api.v1.domain.workflow.Action;
21+
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
22+
import io.mifos.portfolio.api.v1.domain.CostComponent;
23+
import io.mifos.portfolio.api.v1.domain.Payment;
24+
import io.mifos.portfolio.service.internal.util.ChargeInstance;
25+
26+
import java.math.BigDecimal;
27+
import java.util.*;
28+
import java.util.stream.Collectors;
29+
import java.util.stream.Stream;
30+
31+
/**
32+
* @author Myrle Krantz
33+
*/
34+
public class PaymentBuilder {
35+
private final RunningBalances prePaymentBalances;
36+
private final Map<ChargeDefinition, CostComponent> costComponents;
37+
private final Map<String, BigDecimal> balanceAdjustments;
38+
private final boolean accrualAccounting;
39+
40+
PaymentBuilder(final RunningBalances prePaymentBalances,
41+
final boolean accrualAccounting) {
42+
this.prePaymentBalances = prePaymentBalances;
43+
this.costComponents = new HashMap<>();
44+
this.balanceAdjustments = new HashMap<>();
45+
this.accrualAccounting = accrualAccounting;
46+
}
47+
48+
public Payment buildPayment(final Action action, final Set<String> forAccountDesignators) {
49+
50+
if (!forAccountDesignators.isEmpty()) {
51+
final Stream<Map.Entry<ChargeDefinition, CostComponent>> costComponentStream = stream()
52+
.filter(costComponentEntry -> chargeReferencesAccountDesignators(
53+
costComponentEntry.getKey(),
54+
action,
55+
forAccountDesignators));
56+
57+
final List<CostComponent> costComponentList = costComponentStream
58+
.map(costComponentEntry -> new CostComponent(
59+
costComponentEntry.getKey().getIdentifier(),
60+
costComponentEntry.getValue().getAmount()))
61+
.collect(Collectors.toList());
62+
63+
return new Payment(costComponentList, balanceAdjustments);
64+
}
65+
else {
66+
return buildPayment();
67+
}
68+
69+
}
70+
71+
private Payment buildPayment() {
72+
final Stream<Map.Entry<ChargeDefinition, CostComponent>> costComponentStream = stream();
73+
74+
final List<CostComponent> costComponentList = costComponentStream
75+
.map(costComponentEntry -> new CostComponent(
76+
costComponentEntry.getKey().getIdentifier(),
77+
costComponentEntry.getValue().getAmount()))
78+
.collect(Collectors.toList());
79+
80+
return new Payment(costComponentList, balanceAdjustments);
81+
}
82+
83+
PlannedPayment accumulatePlannedPayment(final SimulatedRunningBalances balances) {
84+
final Payment payment = buildPayment();
85+
balanceAdjustments.forEach(balances::adjustBalance);
86+
final Map<String, BigDecimal> balancesCopy = balances.snapshot();
87+
88+
return new PlannedPayment(payment, balancesCopy);
89+
}
90+
91+
public List<ChargeInstance> buildCharges(
92+
final Action action,
93+
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
94+
return stream()
95+
.map(entry -> mapCostComponentEntryToChargeInstance(action, entry, designatorToAccountIdentifierMapper))
96+
.filter(Optional::isPresent)
97+
.map(Optional::get)
98+
.collect(Collectors.toList());
99+
}
100+
101+
BigDecimal getBalanceAdjustment(final String accountDesignator) {
102+
return balanceAdjustments.getOrDefault(accountDesignator, BigDecimal.ZERO);
103+
}
104+
105+
void adjustBalances(
106+
final Action action,
107+
final ChargeDefinition chargeDefinition,
108+
final BigDecimal chargeAmount) {
109+
BigDecimal adjustedChargeAmount = BigDecimal.ZERO;
110+
if (this.accrualAccounting) {
111+
if (chargeIsAccrued(chargeDefinition)) {
112+
if (Action.valueOf(chargeDefinition.getAccrueAction()) == action) {
113+
final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getFromAccountDesignator(), chargeAmount);
114+
adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getAccrualAccountDesignator(), maxDebit);
115+
116+
this.addToBalance(chargeDefinition.getFromAccountDesignator(), adjustedChargeAmount.negate());
117+
this.addToBalance(chargeDefinition.getAccrualAccountDesignator(), adjustedChargeAmount);
118+
} else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
119+
final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getAccrualAccountDesignator(), chargeAmount);
120+
adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getToAccountDesignator(), maxDebit);
121+
122+
this.addToBalance(chargeDefinition.getAccrualAccountDesignator(), adjustedChargeAmount.negate());
123+
this.addToBalance(chargeDefinition.getToAccountDesignator(), adjustedChargeAmount);
124+
}
125+
} else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
126+
final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getFromAccountDesignator(), chargeAmount);
127+
adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getToAccountDesignator(), maxDebit);
128+
129+
this.addToBalance(chargeDefinition.getFromAccountDesignator(), adjustedChargeAmount.negate());
130+
this.addToBalance(chargeDefinition.getToAccountDesignator(), adjustedChargeAmount);
131+
}
132+
}
133+
else if (Action.valueOf(chargeDefinition.getChargeAction()) == action) {
134+
final BigDecimal maxDebit = prePaymentBalances.getMaxDebit(chargeDefinition.getFromAccountDesignator(), chargeAmount);
135+
adjustedChargeAmount = prePaymentBalances.getMaxCredit(chargeDefinition.getToAccountDesignator(), maxDebit);
136+
137+
this.addToBalance(chargeDefinition.getFromAccountDesignator(), adjustedChargeAmount.negate());
138+
this.addToBalance(chargeDefinition.getToAccountDesignator(), adjustedChargeAmount);
139+
}
140+
141+
142+
addToCostComponent(chargeDefinition, adjustedChargeAmount);
143+
}
144+
145+
private static boolean chargeIsAccrued(final ChargeDefinition chargeDefinition) {
146+
return chargeDefinition.getAccrualAccountDesignator() != null;
147+
}
148+
149+
void addToBalance(
150+
final String accountDesignator,
151+
final BigDecimal chargeAmount) {
152+
final BigDecimal currentAdjustment = balanceAdjustments.getOrDefault(accountDesignator, BigDecimal.ZERO);
153+
final BigDecimal newAdjustment = currentAdjustment.add(chargeAmount);
154+
balanceAdjustments.put(accountDesignator, newAdjustment);
155+
}
156+
157+
void addToCostComponent(
158+
final ChargeDefinition chargeDefinition,
159+
final BigDecimal amount) {
160+
final CostComponent costComponent = costComponents
161+
.computeIfAbsent(chargeDefinition, PaymentBuilder::constructEmptyCostComponent);
162+
costComponent.setAmount(costComponent.getAmount().add(amount));
163+
}
164+
165+
private Stream<Map.Entry<ChargeDefinition, CostComponent>> stream() {
166+
return costComponents.entrySet().stream()
167+
.filter(costComponentEntry -> costComponentEntry.getValue().getAmount().compareTo(BigDecimal.ZERO) != 0);
168+
}
169+
170+
171+
private static boolean chargeReferencesAccountDesignators(
172+
final ChargeDefinition chargeDefinition,
173+
final Action action,
174+
final Set<String> forAccountDesignators) {
175+
final Set<String> accountsToCompare = Sets.newHashSet(
176+
chargeDefinition.getFromAccountDesignator(),
177+
chargeDefinition.getToAccountDesignator()
178+
);
179+
if (chargeDefinition.getAccrualAccountDesignator() != null)
180+
accountsToCompare.add(chargeDefinition.getAccrualAccountDesignator());
181+
182+
return !Sets.intersection(accountsToCompare, forAccountDesignators).isEmpty();
183+
}
184+
185+
186+
private static CostComponent constructEmptyCostComponent(final ChargeDefinition chargeDefinition) {
187+
final CostComponent ret = new CostComponent();
188+
ret.setChargeIdentifier(chargeDefinition.getIdentifier());
189+
ret.setAmount(BigDecimal.ZERO);
190+
return ret;
191+
}
192+
193+
private static Optional<ChargeInstance> mapCostComponentEntryToChargeInstance(
194+
final Action action,
195+
final Map.Entry<ChargeDefinition, CostComponent> costComponentEntry,
196+
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
197+
final ChargeDefinition chargeDefinition = costComponentEntry.getKey();
198+
final BigDecimal chargeAmount = costComponentEntry.getValue().getAmount();
199+
200+
if (chargeIsAccrued(chargeDefinition)) {
201+
if (Action.valueOf(chargeDefinition.getAccrueAction()) == action)
202+
return Optional.of(new ChargeInstance(
203+
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
204+
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
205+
chargeAmount));
206+
else if (Action.valueOf(chargeDefinition.getChargeAction()) == action)
207+
return Optional.of(new ChargeInstance(
208+
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getAccrualAccountDesignator()),
209+
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
210+
chargeAmount));
211+
else
212+
return Optional.empty();
213+
}
214+
else if (Action.valueOf(chargeDefinition.getChargeAction()) == action)
215+
return Optional.of(new ChargeInstance(
216+
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getFromAccountDesignator()),
217+
designatorToAccountIdentifierMapper.mapOrThrow(chargeDefinition.getToAccountDesignator()),
218+
chargeAmount));
219+
else
220+
return Optional.empty();
221+
}
222+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2017 Kuelap, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.mifos.individuallending.internal.service;
17+
18+
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
19+
import io.mifos.portfolio.service.internal.util.AccountingAdapter;
20+
import net.jodah.expiringmap.ExpirationPolicy;
21+
import net.jodah.expiringmap.ExpiringMap;
22+
23+
import java.math.BigDecimal;
24+
import java.util.Optional;
25+
import java.util.concurrent.TimeUnit;
26+
27+
/**
28+
* @author Myrle Krantz
29+
*/
30+
public class RealRunningBalances implements RunningBalances {
31+
private final ExpiringMap<String, BigDecimal> realBalanceCache;
32+
33+
34+
RealRunningBalances(
35+
final AccountingAdapter accountingAdapter,
36+
final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper) {
37+
this.realBalanceCache = ExpiringMap.builder()
38+
.maxSize(20)
39+
.expirationPolicy(ExpirationPolicy.CREATED)
40+
.expiration(30,TimeUnit.SECONDS)
41+
.entryLoader((String accountDesignator) -> {
42+
final Optional<String> accountIdentifier;
43+
if (accountDesignator.equals(AccountDesignators.ENTRY)) {
44+
accountIdentifier = designatorToAccountIdentifierMapper.map(accountDesignator);
45+
}
46+
else {
47+
accountIdentifier = Optional.of(designatorToAccountIdentifierMapper.mapOrThrow(accountDesignator));
48+
}
49+
return accountIdentifier.map(accountingAdapter::getCurrentBalance).orElse(BigDecimal.ZERO);
50+
})
51+
.build();
52+
}
53+
54+
@Override
55+
public BigDecimal getBalance(final String accountDesignator) {
56+
return realBalanceCache.get(accountDesignator);
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2017 Kuelap, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.mifos.individuallending.internal.service;
17+
18+
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
19+
20+
import java.math.BigDecimal;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
24+
/**
25+
* @author Myrle Krantz
26+
*/
27+
public interface RunningBalances {
28+
Map<String, BigDecimal> ACCOUNT_SIGNS = new HashMap<String, BigDecimal>() {{
29+
final BigDecimal negative = BigDecimal.valueOf(-1);
30+
final BigDecimal positive = BigDecimal.valueOf(1);
31+
32+
this.put(AccountDesignators.CUSTOMER_LOAN, negative);
33+
this.put(AccountDesignators.LOAN_FUNDS_SOURCE, negative);
34+
this.put(AccountDesignators.PROCESSING_FEE_INCOME, positive);
35+
this.put(AccountDesignators.ORIGINATION_FEE_INCOME, positive);
36+
this.put(AccountDesignators.DISBURSEMENT_FEE_INCOME, positive);
37+
this.put(AccountDesignators.INTEREST_INCOME, positive);
38+
this.put(AccountDesignators.INTEREST_ACCRUAL, positive);
39+
this.put(AccountDesignators.LATE_FEE_INCOME, positive);
40+
this.put(AccountDesignators.LATE_FEE_ACCRUAL, positive);
41+
this.put(AccountDesignators.ARREARS_ALLOWANCE, positive);
42+
this.put(AccountDesignators.ENTRY, positive);
43+
}};
44+
45+
BigDecimal getBalance(final String accountDesignator);
46+
47+
default BigDecimal getMaxDebit(final String accountDesignator, final BigDecimal amount) {
48+
if (accountDesignator.equals(AccountDesignators.ENTRY))
49+
return amount;
50+
51+
if (ACCOUNT_SIGNS.get(accountDesignator).signum() == -1)
52+
return amount;
53+
else
54+
return amount.min(getBalance(accountDesignator));
55+
}
56+
57+
default BigDecimal getMaxCredit(final String accountDesignator, final BigDecimal amount) {
58+
if (accountDesignator.equals(AccountDesignators.ENTRY))
59+
return amount; //don't guard the entry account.
60+
61+
if (ACCOUNT_SIGNS.get(accountDesignator).signum() != -1)
62+
return amount;
63+
else
64+
return amount.min(getBalance(accountDesignator));
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2017 Kuelap, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.mifos.individuallending.internal.service;
18+
19+
import java.math.BigDecimal;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
/**
24+
* @author Myrle Krantz
25+
*/
26+
public class SimulatedRunningBalances implements RunningBalances {
27+
final private Map<String, BigDecimal> balances;
28+
29+
SimulatedRunningBalances() {
30+
this.balances = new HashMap<>();
31+
}
32+
33+
public BigDecimal getBalance(final String accountDesignator) {
34+
return balances.getOrDefault(accountDesignator, BigDecimal.ZERO);
35+
}
36+
37+
void adjustBalance(final String key, final BigDecimal amount) {
38+
final BigDecimal sign = ACCOUNT_SIGNS.get(key);
39+
final BigDecimal currentValue = balances.getOrDefault(key, BigDecimal.ZERO);
40+
final BigDecimal newValue = currentValue.add(amount.multiply(sign));
41+
balances.put(key, newValue);
42+
}
43+
44+
Map<String, BigDecimal> snapshot() {
45+
return new HashMap<>(balances);
46+
}
47+
}

‎service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java

+1-11
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,6 @@ public static ChargeDefinition map(final ChargeDefinitionEntity from) {
9090

9191
private static Boolean readOnlyLegacyMapper(final String identifier) {
9292
switch (identifier) {
93-
case LOAN_FUNDS_ALLOCATION_ID:
94-
return true;
95-
case RETURN_DISBURSEMENT_ID:
96-
return true;
9793
case INTEREST_ID:
9894
return false;
9995
case ALLOW_FOR_WRITE_OFF_ID:
@@ -104,16 +100,12 @@ private static Boolean readOnlyLegacyMapper(final String identifier) {
104100
return false;
105101
case DISBURSE_PAYMENT_ID:
106102
return false;
107-
case TRACK_DISBURSAL_PAYMENT_ID:
108-
return false;
109103
case LOAN_ORIGINATION_FEE_ID:
110104
return true;
111105
case PROCESSING_FEE_ID:
112106
return true;
113107
case REPAYMENT_ID:
114108
return false;
115-
case TRACK_RETURN_PRINCIPAL_ID:
116-
return false;
117109
default:
118110
return false;
119111
}
@@ -126,14 +118,12 @@ private static String proportionalToLegacyMapper(final ChargeDefinitionEntity fr
126118
return from.getProportionalTo();
127119

128120
switch (identifier) {
129-
case LOAN_FUNDS_ALLOCATION_ID:
130-
return ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue();
131121
case LOAN_ORIGINATION_FEE_ID:
132122
return ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue();
133123
case PROCESSING_FEE_ID:
134124
return ChargeProportionalDesignator.MAXIMUM_BALANCE_DESIGNATOR.getValue();
135125
case LATE_FEE_ID:
136-
return ChargeProportionalDesignator.REPAYMENT_DESIGNATOR.getValue();
126+
return ChargeProportionalDesignator.CONTRACTUAL_REPAYMENT_DESIGNATOR.getValue();
137127
default:
138128
return ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR.getValue();
139129
}

‎service/src/main/java/io/mifos/portfolio/service/internal/service/CaseService.java

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import io.mifos.core.lang.ServiceException;
1919
import io.mifos.portfolio.api.v1.domain.Case;
2020
import io.mifos.portfolio.api.v1.domain.CasePage;
21-
import io.mifos.portfolio.api.v1.domain.CostComponent;
21+
import io.mifos.portfolio.api.v1.domain.Payment;
2222
import io.mifos.portfolio.service.internal.mapper.CaseMapper;
2323
import io.mifos.portfolio.service.internal.pattern.PatternFactoryRegistry;
2424
import io.mifos.portfolio.service.internal.repository.CaseEntity;
@@ -133,12 +133,12 @@ public boolean existsByIdentifier(final String productIdentifier,
133133
return this.findByIdentifier(productIdentifier, caseIdentifier).isPresent();
134134
}
135135

136-
public List<CostComponent> getActionCostComponentsForCase(final String productIdentifier,
137-
final String caseIdentifier,
138-
final String actionIdentifier,
139-
final LocalDateTime localDateTime,
140-
final Set<String> forAccountDesignatorsList,
141-
final BigDecimal forPaymentSize) {
136+
public Payment getActionCostComponentsForCase(final String productIdentifier,
137+
final String caseIdentifier,
138+
final String actionIdentifier,
139+
final LocalDateTime localDateTime,
140+
final Set<String> forAccountDesignatorsList,
141+
final BigDecimal forPaymentSize) {
142142
return getPatternFactoryOrThrow(productIdentifier).getCostComponentsForAction(
143143
productIdentifier,
144144
caseIdentifier,

‎service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,12 @@ private static Optional<Creditor> mapToCreditor(final ChargeInstance chargeInsta
156156
public BigDecimal getCurrentBalance(final String accountIdentifier) {
157157
try {
158158
final Account account = ledgerManager.findAccount(accountIdentifier);
159+
if (account == null)
160+
throw ServiceException.internalError("Could not find the account with identifier ''{0}''", accountIdentifier);
159161
return BigDecimal.valueOf(account.getBalance());
160162
}
161163
catch (final AccountNotFoundException e) {
162-
throw ServiceException.internalError("Could not found the account with the identifier ''{0}''", accountIdentifier);
164+
throw ServiceException.internalError("Could not find the account with identifier ''{0}''", accountIdentifier);
163165
}
164166
}
165167

‎service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java

+8-8
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import io.mifos.portfolio.api.v1.domain.Case;
2727
import io.mifos.portfolio.api.v1.domain.CasePage;
2828
import io.mifos.portfolio.api.v1.domain.Command;
29-
import io.mifos.portfolio.api.v1.domain.CostComponent;
29+
import io.mifos.portfolio.api.v1.domain.Payment;
3030
import io.mifos.portfolio.service.internal.checker.CaseChecker;
3131
import io.mifos.portfolio.service.internal.command.ChangeCaseCommand;
3232
import io.mifos.portfolio.service.internal.command.CreateCaseCommand;
@@ -45,7 +45,6 @@
4545
import java.math.BigDecimal;
4646
import java.time.Clock;
4747
import java.time.LocalDateTime;
48-
import java.util.List;
4948
import java.util.Set;
5049

5150
/**
@@ -194,12 +193,13 @@ Set<String> getActionsForCase(@PathVariable("productidentifier") final String pr
194193
produces = MediaType.APPLICATION_JSON_VALUE
195194
)
196195
@ResponseBody
197-
List<CostComponent> getCostComponentsForAction(@PathVariable("productidentifier") final String productIdentifier,
198-
@PathVariable("caseidentifier") final String caseIdentifier,
199-
@PathVariable("actionidentifier") final String actionIdentifier,
200-
@RequestParam(value="fordatetime", required = false, defaultValue = "") final @ValidLocalDateTimeString String forDateTimeString,
201-
@RequestParam(value="touchingaccounts", required = false, defaultValue = "") final Set<String> forAccountDesignators,
202-
@RequestParam(value="forpaymentsize", required = false, defaultValue = "") final BigDecimal forPaymentSize)
196+
Payment getCostComponentsForAction(
197+
@PathVariable("productidentifier") final String productIdentifier,
198+
@PathVariable("caseidentifier") final String caseIdentifier,
199+
@PathVariable("actionidentifier") final String actionIdentifier,
200+
@RequestParam(value="fordatetime", required = false, defaultValue = "") final @ValidLocalDateTimeString String forDateTimeString,
201+
@RequestParam(value="touchingaccounts", required = false, defaultValue = "") final Set<String> forAccountDesignators,
202+
@RequestParam(value="forpaymentsize", required = false, defaultValue = "") final BigDecimal forPaymentSize)
203203
{
204204
checkThatCaseExists(productIdentifier, caseIdentifier);
205205

‎service/src/main/java/io/mifos/products/spi/PatternFactory.java

+2-5
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@
1616
package io.mifos.products.spi;
1717

1818

19-
import io.mifos.portfolio.api.v1.domain.Case;
20-
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
21-
import io.mifos.portfolio.api.v1.domain.CostComponent;
22-
import io.mifos.portfolio.api.v1.domain.Pattern;
19+
import io.mifos.portfolio.api.v1.domain.*;
2320

2421
import java.math.BigDecimal;
2522
import java.time.LocalDateTime;
@@ -38,7 +35,7 @@ public interface PatternFactory {
3835
void changeParameters(Long caseId, String parameters);
3936
Optional<String> getParameters(Long caseId, int minorCurrencyUnitDigits);
4037
Set<String> getNextActionsForState(Case.State state);
41-
List<CostComponent> getCostComponentsForAction(
38+
Payment getCostComponentsForAction(
4239
String productIdentifier,
4340
String caseIdentifier,
4441
String actionIdentifier,

‎service/src/test/java/io/mifos/individuallending/internal/service/CostComponentServiceTest.java

+25-10
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,17 @@
1515
*/
1616
package io.mifos.individuallending.internal.service;
1717

18+
import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
1819
import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
1920
import org.junit.Assert;
2021
import org.junit.Test;
2122
import org.junit.runner.RunWith;
2223
import org.junit.runners.Parameterized;
2324

2425
import java.math.BigDecimal;
25-
import java.util.ArrayList;
26-
import java.util.Collection;
27-
import java.util.Collections;
26+
import java.util.*;
2827

29-
import static io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator.PRINCIPAL_ADJUSTMENT_DESIGNATOR;
28+
import static io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR;
3029
import static io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator.RUNNING_BALANCE_DESIGNATOR;
3130

3231
/**
@@ -40,7 +39,7 @@ private static class TestCase {
4039
BigDecimal maximumBalance = BigDecimal.ZERO;
4140
BigDecimal runningBalance = BigDecimal.ZERO;
4241
BigDecimal loanPaymentSize = BigDecimal.ZERO;
43-
BigDecimal expectedAmount = BigDecimal.ZERO;
42+
BigDecimal expectedAmount = BigDecimal.ONE;
4443

4544
private TestCase(String description) {
4645
this.description = description;
@@ -70,16 +69,28 @@ TestCase expectedAmount(BigDecimal expectedAmount) {
7069
this.expectedAmount = expectedAmount;
7170
return this;
7271
}
72+
73+
@Override
74+
public String toString() {
75+
return "TestCase{" +
76+
"description='" + description + '\'' +
77+
", chargeProportionalDesignator=" + chargeProportionalDesignator +
78+
", maximumBalance=" + maximumBalance +
79+
", runningBalance=" + runningBalance +
80+
", loanPaymentSize=" + loanPaymentSize +
81+
", expectedAmount=" + expectedAmount +
82+
'}';
83+
}
7384
}
7485

7586
@Parameterized.Parameters
7687
public static Collection testCases() {
7788
final Collection<CostComponentServiceTest.TestCase> ret = new ArrayList<>();
7889
ret.add(new TestCase("simple"));
7990
ret.add(new TestCase("distribution fee")
80-
.chargeProportionalDesignator(PRINCIPAL_ADJUSTMENT_DESIGNATOR)
91+
.chargeProportionalDesignator(REQUESTED_DISBURSEMENT_DESIGNATOR)
8192
.maximumBalance(BigDecimal.valueOf(2000))
82-
.loanPaymentSize(BigDecimal.valueOf(-2000))
93+
.loanPaymentSize(BigDecimal.valueOf(2000))
8394
.expectedAmount(BigDecimal.valueOf(2000)));
8495
ret.add(new TestCase("origination fee")
8596
.chargeProportionalDesignator(RUNNING_BALANCE_DESIGNATOR)
@@ -96,14 +107,18 @@ public CostComponentServiceTest(final CostComponentServiceTest.TestCase testCase
96107

97108
@Test
98109
public void getAmountProportionalTo() {
110+
final SimulatedRunningBalances runningBalances = new SimulatedRunningBalances();
111+
runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN, testCase.runningBalance.negate());
99112
final BigDecimal amount = CostComponentService.getAmountProportionalTo(
100113
testCase.chargeProportionalDesignator,
101114
testCase.maximumBalance,
102-
testCase.runningBalance,
115+
runningBalances,
116+
testCase.loanPaymentSize,
117+
testCase.loanPaymentSize,
103118
testCase.loanPaymentSize,
104-
Collections.emptyMap());
119+
new PaymentBuilder(runningBalances, false));
105120

106-
Assert.assertEquals(testCase.expectedAmount, amount);
121+
Assert.assertEquals(testCase.toString(), testCase.expectedAmount, amount);
107122
}
108123

109124
}

‎service/src/test/java/io/mifos/individuallending/internal/service/IndividualLoanServiceTest.java

+27-45
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
2525
import io.mifos.individuallending.api.v1.domain.workflow.Action;
2626
import io.mifos.individuallending.internal.mapper.CaseParametersMapper;
27-
import io.mifos.portfolio.api.v1.domain.*;
27+
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
28+
import io.mifos.portfolio.api.v1.domain.CostComponent;
29+
import io.mifos.portfolio.api.v1.domain.PaymentCycle;
30+
import io.mifos.portfolio.api.v1.domain.TermRange;
2831
import io.mifos.portfolio.service.internal.repository.BalanceSegmentRepository;
2932
import io.mifos.portfolio.service.internal.repository.CaseEntity;
3033
import io.mifos.portfolio.service.internal.repository.ProductEntity;
@@ -94,14 +97,10 @@ private static class TestCase {
9497
private BigDecimal interest;
9598
private Set<String> expectedChargeIdentifiers = new HashSet<>(Arrays.asList(
9699
PROCESSING_FEE_ID,
97-
LOAN_FUNDS_ALLOCATION_ID,
98-
RETURN_DISBURSEMENT_ID,
99100
LOAN_ORIGINATION_FEE_ID,
100101
INTEREST_ID,
101102
DISBURSEMENT_FEE_ID,
102103
REPAYMENT_ID,
103-
TRACK_DISBURSAL_PAYMENT_ID,
104-
TRACK_RETURN_PRINCIPAL_ID,
105104
DISBURSE_PAYMENT_ID,
106105
LATE_FEE_ID
107106
));
@@ -189,8 +188,8 @@ private static TestCase simpleCase()
189188
caseParameters.setTermRange(new TermRange(ChronoUnit.WEEKS, 3));
190189
caseParameters.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, 0, null, null));
191190

192-
final ChargeDefinition processingFeeCharge = getFixedSingleChargeDefinition(10.0, Action.OPEN, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME);
193-
final ChargeDefinition loanOriginationFeeCharge = getFixedSingleChargeDefinition(100.0, Action.APPROVE, LOAN_ORIGINATION_FEE_ID, AccountDesignators.ORIGINATION_FEE_INCOME);
191+
final ChargeDefinition processingFeeCharge = getFixedSingleChargeDefinition(10.0, Action.DISBURSE, PROCESSING_FEE_ID, AccountDesignators.PROCESSING_FEE_INCOME);
192+
final ChargeDefinition loanOriginationFeeCharge = getFixedSingleChargeDefinition(100.0, Action.DISBURSE, LOAN_ORIGINATION_FEE_ID, AccountDesignators.ORIGINATION_FEE_INCOME);
194193
final List<ChargeDefinition> defaultChargesWithFeesReplaced =
195194
charges().stream().map(x -> {
196195
switch (x.getIdentifier()) {
@@ -209,9 +208,7 @@ private static TestCase simpleCase()
209208
.initialDisbursementDate(initialDisbursementDate)
210209
.chargeDefinitions(defaultChargesWithFeesReplaced)
211210
.interest(BigDecimal.valueOf(1))
212-
.expectChargeInstancesForActionDatePair(Action.OPEN, initialDisbursementDate, Collections.singletonList(processingFeeCharge))
213-
.expectChargeInstancesForActionDatePair(Action.APPROVE, initialDisbursementDate,
214-
Collections.singletonList(loanOriginationFeeCharge));
211+
.expectChargeInstancesForActionDatePair(Action.DISBURSE, initialDisbursementDate, Arrays.asList(processingFeeCharge, loanOriginationFeeCharge));
215212
}
216213

217214
private static TestCase yearLoanTestCase()
@@ -267,7 +264,7 @@ private static ChargeDefinition getFixedSingleChargeDefinition(
267264
ret.setChargeAction(action.name());
268265
ret.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
269266
ret.setProportionalTo(null);
270-
ret.setFromAccountDesignator(AccountDesignators.ENTRY);
267+
ret.setFromAccountDesignator(AccountDesignators.CUSTOMER_LOAN);
271268
ret.setToAccountDesignator(feeAccountDesignator);
272269
ret.setForCycleSizeUnit(null);
273270
return ret;
@@ -318,38 +315,42 @@ public void getPlannedPayments() throws Exception {
318315
final Set<BigDecimal> customerRepayments = Stream.iterate(1, x -> x + 1).limit(allPlannedPayments.size() - 1)
319316
.map(x ->
320317
{
321-
final BigDecimal costComponentSum = allPlannedPayments.get(x).getCostComponents().stream()
322-
.filter(this::includeCostComponentsInSumCheck)
318+
final BigDecimal valueOfRepaymentCostComponent = allPlannedPayments.get(x).getPayment().getCostComponents().stream()
319+
.filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.REPAYMENT_ID))
323320
.map(CostComponent::getAmount)
324321
.reduce(BigDecimal::add)
325322
.orElse(BigDecimal.ZERO);
326-
final BigDecimal valueOfPrincipleTrackingCostComponent = allPlannedPayments.get(x).getCostComponents().stream()
327-
.filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID))
323+
final BigDecimal valueOfInterestCostComponent = allPlannedPayments.get(x).getPayment().getCostComponents().stream()
324+
.filter(costComponent -> costComponent.getChargeIdentifier().equals(ChargeIdentifiers.INTEREST_ID))
328325
.map(CostComponent::getAmount)
329326
.reduce(BigDecimal::add)
330327
.orElse(BigDecimal.ZERO);
331-
final BigDecimal principalDifference = allPlannedPayments.get(x-1).getRemainingPrincipal().subtract(allPlannedPayments.get(x).getRemainingPrincipal());
332-
Assert.assertEquals(valueOfPrincipleTrackingCostComponent, principalDifference);
328+
final BigDecimal principalDifference = allPlannedPayments.get(x-1).getBalances().get(AccountDesignators.CUSTOMER_LOAN).subtract(allPlannedPayments.get(x).getBalances().get(AccountDesignators.CUSTOMER_LOAN));
329+
Assert.assertEquals("Checking payment " + x, valueOfRepaymentCostComponent.subtract(valueOfInterestCostComponent), principalDifference);
333330
Assert.assertNotEquals("Remaining principle should always be positive or zero.",
334-
allPlannedPayments.get(x).getRemainingPrincipal().signum(), -1);
335-
final boolean containsLateFee = allPlannedPayments.get(x).getCostComponents().stream().anyMatch(y -> y.getChargeIdentifier().equals(LATE_FEE_ID));
331+
allPlannedPayments.get(x).getBalances().get(AccountDesignators.CUSTOMER_LOAN).signum(), -1);
332+
final boolean containsLateFee = allPlannedPayments.get(x).getPayment().getCostComponents().stream().anyMatch(y -> y.getChargeIdentifier().equals(LATE_FEE_ID));
336333
Assert.assertFalse("Late fee should not be included in planned payments", containsLateFee);
337-
return costComponentSum;
334+
return valueOfRepaymentCostComponent;
338335
}
339336
).collect(Collectors.toSet());
340337

341338
//All entries should have the correct scale.
342339
allPlannedPayments.forEach(x -> {
343-
x.getCostComponents().forEach(y -> Assert.assertEquals(testCase.minorCurrencyUnitDigits, y.getAmount().scale()));
344-
Assert.assertEquals(testCase.minorCurrencyUnitDigits, x.getRemainingPrincipal().scale());
345-
final int uniqueChargeIdentifierCount = x.getCostComponents().stream()
340+
x.getPayment().getCostComponents().forEach(y -> Assert.assertEquals(testCase.minorCurrencyUnitDigits, y.getAmount().scale()));
341+
Assert.assertEquals(testCase.minorCurrencyUnitDigits, x.getBalances().get(AccountDesignators.CUSTOMER_LOAN).scale());
342+
final int uniqueChargeIdentifierCount = x.getPayment().getCostComponents().stream()
346343
.map(CostComponent::getChargeIdentifier)
347344
.collect(Collectors.toSet())
348345
.size();
349346
Assert.assertEquals("There should be only one cost component per charge per planned payment.",
350-
x.getCostComponents().size(), uniqueChargeIdentifierCount);
347+
x.getPayment().getCostComponents().size(), uniqueChargeIdentifierCount);
351348
});
352349

350+
Assert.assertEquals("Final balance should be zero.",
351+
BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
352+
allPlannedPayments.get(allPlannedPayments.size()-1).getBalances().get(AccountDesignators.CUSTOMER_LOAN));
353+
353354
//All customer payments should be within one percent of each other.
354355
final Optional<BigDecimal> maxPayment = customerRepayments.stream().max(BigDecimal::compareTo);
355356
final Optional<BigDecimal> minPayment = customerRepayments.stream().min(BigDecimal::compareTo);
@@ -359,10 +360,6 @@ public void getPlannedPayments() throws Exception {
359360
Assert.assertTrue("Percent difference = " + percentDifference + ", max = " + maxPayment.get() + ", min = " + minPayment.get(),
360361
percentDifference < 0.01);
361362

362-
//Final balance should be zero.
363-
Assert.assertEquals(BigDecimal.ZERO.setScale(testCase.minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN),
364-
allPlannedPayments.get(allPlannedPayments.size()-1).getRemainingPrincipal());
365-
366363
//All charge identifiers should be associated with a name on the returned page.
367364
final Set<String> resultChargeIdentifiers = firstPage.getChargeNames().stream()
368365
.map(ChargeName::getIdentifier)
@@ -371,22 +368,6 @@ public void getPlannedPayments() throws Exception {
371368
Assert.assertEquals(testCase.expectedChargeIdentifiers, resultChargeIdentifiers);
372369
}
373370

374-
private boolean includeCostComponentsInSumCheck(CostComponent costComponent) {
375-
switch (costComponent.getChargeIdentifier()) {
376-
case ChargeIdentifiers.INTEREST_ID:
377-
case ChargeIdentifiers.DISBURSEMENT_FEE_ID:
378-
case ChargeIdentifiers.TRACK_DISBURSAL_PAYMENT_ID:
379-
case ChargeIdentifiers.LATE_FEE_ID:
380-
case ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID:
381-
case ChargeIdentifiers.TRACK_RETURN_PRINCIPAL_ID:
382-
case ChargeIdentifiers.PROCESSING_FEE_ID:
383-
return true;
384-
default:
385-
return false;
386-
387-
}
388-
}
389-
390371
@Test
391372
public void getScheduledCharges() {
392373
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(testCase.initialDisbursementDate, testCase.caseParameters);
@@ -423,7 +404,8 @@ public void getScheduledCharges() {
423404
new ActionDatePair(scheduledCharge.getScheduledAction().action, scheduledCharge.getScheduledAction().when),
424405
Collectors.mapping(ScheduledCharge::getChargeDefinition, Collectors.toSet())));
425406

426-
testCase.chargeDefinitionsForActions.forEach((key, value) -> value.forEach(x -> Assert.assertTrue(searchableScheduledCharges.get(key).contains(x))));
407+
testCase.chargeDefinitionsForActions.forEach((key, value) ->
408+
value.forEach(x -> Assert.assertTrue(searchableScheduledCharges.get(key).contains(x))));
427409
}
428410

429411
private double percentDifference(final BigDecimal maxPayment, final BigDecimal minPayment) {

‎shared.gradle

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ ext.versions = [
1414
mifosrhythm : '0.1.0-BUILD-SNAPSHOT',
1515
mifoscustomer : '0.1.0-BUILD-SNAPSHOT',
1616
validator : '5.3.0.Final',
17-
javamoneylib : '0.9-SNAPSHOT'
17+
javamoneylib : '0.9-SNAPSHOT',
18+
expiringmap : '0.5.8'
1819
]
1920

2021
apply plugin: 'java'

0 commit comments

Comments
 (0)
Please sign in to comment.