Write off of principal implemented and tested.
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java
index fb78e89..424d937 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/AccountDesignators.java
@@ -37,6 +37,6 @@
String LATE_FEE_ACCRUAL = "lfa";
String PRODUCT_LOSS_ALLOWANCE = "pa";
String GENERAL_LOSS_ALLOWANCE = "aa";
- String GENERAL_EXPENSE = "ge";
+ String EXPENSE = "ee";
String ENTRY = "ey";
}
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
index 8167c85..ce9600b 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/product/ChargeIdentifiers.java
@@ -24,8 +24,6 @@
public interface ChargeIdentifiers {
String INTEREST_NAME = "Interest";
String INTEREST_ID = "interest";
- String ALLOW_FOR_WRITE_OFF_NAME = "Allow for write-off";
- String ALLOW_FOR_WRITE_OFF_ID = "allow-for-write-off";
String LATE_FEE_NAME = "Late fee";
String LATE_FEE_ID = "late-fee";
String DISBURSEMENT_FEE_NAME = "Disbursement fee";
@@ -44,6 +42,8 @@
String REPAY_FEES_ID = "repay-fees";
String PROVISION_FOR_LOSSES_NAME = "Provision for losses";
String PROVISION_FOR_LOSSES_ID = "loss-provisioning";
+ String WRITE_OFF_NAME = "Write off";
+ String WRITE_OFF_ID = "write-off";
static String nameToIdentifier(String name) {
return name.toLowerCase(Locale.US).replace(" ", "-");
diff --git a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
index 8d46e36..1362783 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -56,7 +56,6 @@
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.math.BigDecimal;
-import java.time.Clock;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
@@ -192,40 +191,41 @@
return caseInstance;
}
- void checkStateTransfer(final String productIdentifier,
- final String caseIdentifier,
- final Action action,
- final List<AccountAssignment> oneTimeAccountAssignments,
- final String event,
- final Case.State nextState) throws InterruptedException {
+ void checkStateTransfer(
+ final String productIdentifier,
+ final String caseIdentifier,
+ final Action action,
+ final LocalDateTime actionDateTime,
+ final List<AccountAssignment> oneTimeAccountAssignments,
+ final String event,
+ final Case.State nextState) throws InterruptedException {
checkStateTransfer(
productIdentifier,
caseIdentifier,
action,
- LocalDateTime.now(Clock.systemUTC()),
+ actionDateTime,
oneTimeAccountAssignments,
BigDecimal.ZERO,
event,
- midnightToday(),
nextState);
}
- void checkStateTransfer(final String productIdentifier,
- final String caseIdentifier,
- final Action action,
- final LocalDateTime actionDateTime,
- final List<AccountAssignment> oneTimeAccountAssignments,
- final BigDecimal paymentSize,
- final String event,
- final LocalDateTime eventDateTime,
- final Case.State nextState) throws InterruptedException {
+ void checkStateTransfer(
+ final String productIdentifier,
+ final String caseIdentifier,
+ final Action action,
+ final LocalDateTime actionDateTime,
+ final List<AccountAssignment> oneTimeAccountAssignments,
+ final BigDecimal paymentSize,
+ final String event,
+ final Case.State nextState) throws InterruptedException {
final Command command = new Command();
command.setOneTimeAccountAssignments(oneTimeAccountAssignments);
command.setPaymentSize(paymentSize);
command.setCreatedOn(DateConverter.toIsoString(actionDateTime));
portfolioManager.executeCaseCommand(productIdentifier, caseIdentifier, action.name(), command);
- Assert.assertTrue(eventRecorder.wait(event, new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(eventDateTime))));
+ Assert.assertTrue(eventRecorder.wait(event, new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(actionDateTime))));
final Case customerCase = portfolioManager.getCase(productIdentifier, caseIdentifier);
Assert.assertEquals(nextState.name(), customerCase.getCurrentState());
@@ -314,6 +314,13 @@
return entryAccountAssignment;
}
+ AccountAssignment assignExpenseToGeneralExpense() {
+ final AccountAssignment entryAccountAssignment = new AccountAssignment();
+ entryAccountAssignment.setDesignator(AccountDesignators.EXPENSE);
+ entryAccountAssignment.setAccountIdentifier(AccountingFixture.GENERAL_EXPENSE_ACCOUNT_IDENTIFIER);
+ return entryAccountAssignment;
+ }
+
TaskDefinition createTaskDefinition(Product product) throws InterruptedException {
final TaskDefinition taskDefinition = getTaskDefinition();
portfolioManager.createTaskDefinition(product.getIdentifier(), taskDefinition);
diff --git a/component-test/src/main/java/io/mifos/portfolio/Fixture.java b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
index 092052b..ffd241c 100644
--- a/component-test/src/main/java/io/mifos/portfolio/Fixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/Fixture.java
@@ -69,9 +69,9 @@
accountAssignments.add(new AccountAssignment(LATE_FEE_ACCRUAL, LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(PRODUCT_LOSS_ALLOWANCE, PRODUCT_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER));
accountAssignments.add(new AccountAssignment(GENERAL_LOSS_ALLOWANCE, GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER));
- accountAssignments.add(new AccountAssignment(GENERAL_EXPENSE, GENERAL_EXPENSE_ACCOUNT_IDENTIFIER));
+ //accountAssignments.add(new AccountAssignment(EXPENSE, ...));
//accountAssignments.add(new AccountAssignment(ENTRY, ...));
- // Don't assign entry account in test since it usually will not be assigned IRL.
+ // Don't assign entry and expense accounts in test since they usually will not be assigned IRL.
accountAssignments.add(new AccountAssignment(LOAN_FUNDS_SOURCE, LOAN_FUNDS_SOURCE_ACCOUNT_IDENTIFIER));
final AccountAssignment customerLoanPrincipalAccountAssignment = new AccountAssignment();
customerLoanPrincipalAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
index 1210896..0043728 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -42,7 +42,6 @@
import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.math.RoundingMode;
-import java.time.Clock;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
@@ -322,7 +321,7 @@
today,
1,
8,
- 8,
+ 7,
lateFee);
step6ICalculateInterestAndLossAllowancesForLateLoanForRangeOfDays(
today,
@@ -332,7 +331,7 @@
new LossProvisionStep(60, BigDecimal.valueOf(60))
);
- //step8IWriteOff(today.plusDays(61));
+ step8IWriteOff(today.plusDays(68));
}
private BigDecimal findNextRepaymentAmount(
@@ -439,6 +438,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.OPEN,
+ forDateTime,
Collections.singletonList(assignEntryToTeller()),
IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE,
Case.State.PENDING);
@@ -461,6 +461,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DENY,
+ forDateTime,
Collections.singletonList(assignEntryToTeller()),
IndividualLoanEventConstants.DENY_INDIVIDUALLOAN_CASE,
Case.State.CLOSED);
@@ -487,6 +488,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.APPROVE,
+ forDateTime,
Collections.singletonList(assignEntryToTeller()),
IndividualLoanEventConstants.APPROVE_INDIVIDUALLOAN_CASE,
Case.State.APPROVED);
@@ -535,11 +537,10 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE,
- LocalDateTime.now(Clock.systemUTC()),
+ forDateTime,
Collections.singletonList(assignEntryToTeller()),
amount,
IndividualLoanEventConstants.DISBURSE_INDIVIDUALLOAN_CASE,
- midnightToday(),
Case.State.ACTIVE);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPLY_INTEREST,
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.MARK_IN_ARREARS, Action.WRITE_OFF, Action.CLOSE);
@@ -625,8 +626,14 @@
.multiply(dailyInterestRate)
.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
+ final BigDecimal provisionForLosses = calculatedLateFee.equals(BigDecimal.ZERO) ?
+ BigDecimal.ZERO :
+ expectedCurrentPrincipal.multiply(BigDecimal.valueOf(0.09))
+ .setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
+
logger.info("calculatedInterest '{}'", calculatedInterest);
logger.info("calculatedLateFee '{}'", calculatedLateFee);
+ logger.info("provisionForLosses '{}'", provisionForLosses);
checkCostComponentForActionCorrect(
@@ -646,7 +653,8 @@
null,
null,
forDateTime,
- MINOR_CURRENCY_UNIT_DIGITS);
+ MINOR_CURRENCY_UNIT_DIGITS,
+ new CostComponent(ChargeIdentifiers.PROVISION_FOR_LOSSES_ID, provisionForLosses.negate()));
}
final BeatPublish interestBeat = new BeatPublish(beatIdentifier, midnightTimeStamp);
portfolioBeatListener.publishBeat(interestBeat);
@@ -685,10 +693,6 @@
if (calculatedLateFee.compareTo(BigDecimal.ZERO) != 0) {
- final BigDecimal provisionForLosses =
- expectedCurrentPrincipal.multiply(BigDecimal.valueOf(0.09))
- .setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
-
final Set<Debtor> lateFeeDebtors = new HashSet<>();
lateFeeDebtors.add(new Debtor(
customerLoanFeeIdentifier,
@@ -728,12 +732,14 @@
final Map<Integer, BigDecimal> lossProvisionConfiguration = Stream.of(lossProvisionSteps)
.collect(Collectors.toMap(LossProvisionStep::getDaysLate, LossProvisionStep::getPercentProvision));
- IntStream.rangeClosed(9, 60)
+ IntStream.rangeClosed(9, 67)
.forEach(day -> {
try {
+ final int daysLate = day - 7;
step6ICalculateInterestAndLossAllowancesForLateLoan(
referenceDate.plusDays(day),
- lossProvisionConfiguration.get(day-7));
+ daysLate,
+ lossProvisionConfiguration.get(daysLate));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
@@ -750,6 +756,7 @@
private void step6ICalculateInterestAndLossAllowancesForLateLoan(
final LocalDateTime forDateTime,
+ final int daysLate,
final @Nullable BigDecimal percentProvision) throws InterruptedException
{
logger.info("step6ICalculateInterestAndLossAllowancesForLateLoan '{}'", forDateTime);
@@ -764,8 +771,14 @@
.multiply(dailyInterestRate)
.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
+ final BigDecimal provisionForLosses = percentProvision == null ?
+ BigDecimal.ZERO :
+ expectedCurrentPrincipal.multiply(percentProvision.divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_EVEN))
+ .setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
+
logger.info("calculatedInterest '{}'", calculatedInterest);
logger.info("percentProvision '{}'", percentProvision);
+ logger.info("provisionForLosses '{}'", provisionForLosses);
checkCostComponentForActionCorrect(
@@ -777,16 +790,16 @@
forDateTime,
MINOR_CURRENCY_UNIT_DIGITS);
- if (percentProvision != null) {
- checkCostComponentForActionCorrect(
- product.getIdentifier(),
- customerCase.getIdentifier(),
- Action.MARK_IN_ARREARS,
- null,
- null,
- forDateTime,
- MINOR_CURRENCY_UNIT_DIGITS);
- }
+ checkCostComponentForActionCorrect(
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ Action.MARK_IN_ARREARS,
+ null,
+ BigDecimal.valueOf(daysLate),
+ forDateTime,
+ MINOR_CURRENCY_UNIT_DIGITS,
+ new CostComponent(ChargeIdentifiers.PROVISION_FOR_LOSSES_ID, provisionForLosses.negate()));
+
final BeatPublish interestBeat = new BeatPublish(beatIdentifier, midnightTimeStamp);
portfolioBeatListener.publishBeat(interestBeat);
Assert.assertTrue(this.eventRecorder.wait(io.mifos.rhythm.spi.v1.events.EventConstants.POST_PUBLISHEDBEAT,
@@ -824,20 +837,15 @@
customerCase.getIdentifier(), Action.APPLY_INTEREST);
if (percentProvision != null) {
- final BigDecimal calculatedProvisionForLosses =
- expectedCurrentPrincipal.multiply(percentProvision.divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_EVEN))
- .setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
- logger.info("calculatedProvisionForLosses '{}'", calculatedProvisionForLosses);
-
final Set<Debtor> lateFeeDebtors = new HashSet<>();
lateFeeDebtors.add(new Debtor(
AccountingFixture.GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER,
- calculatedProvisionForLosses.toPlainString()));
+ provisionForLosses.toPlainString()));
final Set<Creditor> lateFeeCreditors = new HashSet<>();
lateFeeCreditors.add(new Creditor(
AccountingFixture.PRODUCT_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER,
- calculatedProvisionForLosses.toPlainString()));
+ provisionForLosses.toPlainString()));
AccountingFixture.verifyTransfer(
ledgerManager,
lateFeeDebtors,
@@ -845,7 +853,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.MARK_IN_ARREARS);
- productLossAllowance = productLossAllowance.add(calculatedProvisionForLosses);
+ productLossAllowance = productLossAllowance.add(provisionForLosses);
}
interestAccrued = interestAccrued.add(calculatedInterest);
@@ -882,7 +890,6 @@
Collections.singletonList(assignEntryToTeller()),
amount,
IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
- midnightToday(),
Case.State.ACTIVE); //Close has to be done explicitly.
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPLY_INTEREST,
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.MARK_IN_ARREARS, Action.WRITE_OFF, Action.CLOSE);
@@ -929,7 +936,7 @@
private void step8Close(
final LocalDateTime forDateTime) throws InterruptedException
{
- logger.info("step8Close");
+ logger.info("step8Close for '{}'", forDateTime);
checkCostComponentForActionCorrect(
product.getIdentifier(),
@@ -943,6 +950,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.CLOSE,
+ forDateTime,
Collections.singletonList(assignEntryToTeller()),
IndividualLoanEventConstants.CLOSE_INDIVIDUALLOAN_CASE,
Case.State.CLOSED); //Close has to be done explicitly.
@@ -952,7 +960,7 @@
private void step8IWriteOff(
final LocalDateTime forDateTime) throws InterruptedException {
- logger.info("step8IWriteOff");
+ logger.info("step8IWriteOff for '{}'", forDateTime);
checkCostComponentForActionCorrect(
product.getIdentifier(),
@@ -961,16 +969,32 @@
null,
null,
forDateTime,
- MINOR_CURRENCY_UNIT_DIGITS);
+ MINOR_CURRENCY_UNIT_DIGITS,
+ new CostComponent(ChargeIdentifiers.WRITE_OFF_ID, expectedCurrentPrincipal));
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.WRITE_OFF,
- Collections.singletonList(assignEntryToTeller()),
+ forDateTime,
+ Collections.singletonList(assignExpenseToGeneralExpense()),
IndividualLoanEventConstants.WRITE_OFF_INDIVIDUALLOAN_CASE,
Case.State.CLOSED); //Close has to be done explicitly.
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
+
+ final Set<Debtor> debtors = new HashSet<>();
+ debtors.add(new Debtor(AccountingFixture.GENERAL_EXPENSE_ACCOUNT_IDENTIFIER, expectedCurrentPrincipal.toPlainString()));
+ //TODO:debtors.add(new Debtor(AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER, lateFees.toPlainString()));
+ //TODO:debtors.add(new Debtor(AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
+
+ final Set<Creditor> creditors = new HashSet<>();
+ creditors.add(new Creditor(AccountingFixture.GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER, expectedCurrentPrincipal.toPlainString()));
+ //TODO:creditors.add(new Creditor(AccountingFixture.PRODUCT_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER, lateFees.add(interestAccrued).toPlainString()));
+
+ AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.WRITE_OFF);
+
+ productLossAllowance = BigDecimal.ZERO;
+ updateBalanceMock();
}
private void updateBalanceMock() {
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
index f184143..e6a2828 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
@@ -20,7 +20,6 @@
import io.mifos.portfolio.api.v1.domain.Product;
import org.junit.Test;
-import java.math.BigDecimal;
import java.time.Clock;
import java.time.LocalDateTime;
import java.util.Collections;
@@ -36,57 +35,6 @@
//public void testHappyWorkflow() throws InterruptedException
@Test
- public void testBadCustomerWorkflow() throws InterruptedException {
- final Product product = createAndEnableProduct();
- final Case customerCase = createCase(product.getIdentifier());
-
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN);
-
-
- checkStateTransfer(
- product.getIdentifier(),
- customerCase.getIdentifier(),
- Action.OPEN,
- Collections.singletonList(assignEntryToTeller()),
- OPEN_INDIVIDUALLOAN_CASE,
- Case.State.PENDING);
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Action.DENY);
-
-
- checkStateTransfer(
- product.getIdentifier(),
- customerCase.getIdentifier(),
- Action.APPROVE,
- Collections.singletonList(assignEntryToTeller()),
- APPROVE_INDIVIDUALLOAN_CASE,
- Case.State.APPROVED);
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Action.CLOSE);
-
-
- checkStateTransfer(
- product.getIdentifier(),
- customerCase.getIdentifier(),
- Action.DISBURSE,
- LocalDateTime.now(Clock.systemUTC()),
- Collections.singletonList(assignEntryToTeller()),
- BigDecimal.valueOf(2000L),
- DISBURSE_INDIVIDUALLOAN_CASE,
- midnightToday(),
- Case.State.ACTIVE);
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
- Action.APPLY_INTEREST, Action.MARK_LATE, Action.MARK_IN_ARREARS, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.WRITE_OFF, Action.CLOSE);
-
- checkStateTransfer(
- product.getIdentifier(),
- customerCase.getIdentifier(),
- Action.WRITE_OFF,
- Collections.singletonList(assignEntryToTeller()),
- WRITE_OFF_INDIVIDUALLOAN_CASE,
- Case.State.CLOSED);
- checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
- }
-
- @Test
public void testApproveBeforeOpen() throws InterruptedException {
final Product product = createAndEnableProduct();
final Case customerCase = createCase(product.getIdentifier());
@@ -109,6 +57,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.OPEN,
+ LocalDateTime.now(Clock.systemUTC()),
Collections.singletonList(assignEntryToTeller()),
OPEN_INDIVIDUALLOAN_CASE,
Case.State.PENDING);
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java b/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java
index 0fef315..5aa6e7a 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java
@@ -32,6 +32,8 @@
import org.junit.Assert;
import org.junit.Test;
+import java.time.Clock;
+import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
@@ -204,6 +206,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.OPEN,
+ LocalDateTime.now(Clock.systemUTC()),
Collections.singletonList(assignEntryToTeller()),
IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE,
Case.State.PENDING);
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index c286fcd..bf37c9c 100644
--- a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
+++ b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
@@ -106,7 +106,7 @@
AccountDesignators.GENERAL_LOSS_ALLOWANCE,
AccountType.EXPENSE.name()));
individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
- AccountDesignators.GENERAL_EXPENSE,
+ AccountDesignators.EXPENSE,
AccountType.EXPENSE.name()));
individualLendingRequiredAccounts.add(new RequiredAccountAssignment(
AccountDesignators.ENTRY,
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
index 840ab12..a12b605 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
@@ -48,8 +48,6 @@
import javax.annotation.Nullable;
import java.math.BigDecimal;
-import java.time.Clock;
-import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Collections;
@@ -146,7 +144,6 @@
final PaymentBuilder paymentBuilder
= openPaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, CostComponentService.today(), runningBalances);
- final LocalDateTime today = today();
final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
designatorToAccountIdentifierMapper,
@@ -167,7 +164,7 @@
customerCase.setCurrentState(Case.State.PENDING.name());
caseRepository.save(customerCase);
- return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getCommand().getCreatedOn());
}
@Transactional
@@ -193,7 +190,6 @@
final PaymentBuilder paymentBuilder
= denyPaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, CostComponentService.today(), runningBalances);
- final LocalDateTime today = today();
final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
designatorToAccountIdentifierMapper,
@@ -213,7 +209,7 @@
customerCase.setCurrentState(Case.State.CLOSED.name());
caseRepository.save(customerCase);
- return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getCommand().getCreatedOn());
}
static class InterruptedInALambdaException extends RuntimeException {
@@ -288,8 +284,6 @@
final PaymentBuilder paymentBuilder =
approvePaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, CostComponentService.today(), runningBalances);
- final LocalDateTime today = today();
-
final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
designatorToAccountIdentifierMapper,
command.getCommand().getNote(),
@@ -309,7 +303,7 @@
customerCase.setCurrentState(Case.State.APPROVED.name());
caseRepository.save(customerCase);
- return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getCommand().getCreatedOn());
}
@Transactional
@@ -335,8 +329,6 @@
final PaymentBuilder paymentBuilder =
disbursePaymentBuilderService.getPaymentBuilder(dataContextOfAction, disbursalAmount, CostComponentService.today(), runningBalances);
- final LocalDateTime today = today();
-
final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
designatorToAccountIdentifierMapper,
command.getCommand().getNote(),
@@ -355,7 +347,7 @@
//Only move to new state if book charges command was accepted.
if (Case.State.valueOf(dataContextOfAction.getCustomerCaseEntity().getCurrentState()) != Case.State.ACTIVE) {
final LocalDateTime endOfTerm
- = ScheduledActionHelpers.getRoughEndDate(today.toLocalDate(), dataContextOfAction.getCaseParameters())
+ = ScheduledActionHelpers.getRoughEndDate(DateConverter.fromIsoString(command.getCommand().getCreatedOn()).toLocalDate(), dataContextOfAction.getCaseParameters())
.atTime(LocalTime.MIDNIGHT);
customerCase.setEndOfTerm(endOfTerm);
customerCase.setCurrentState(Case.State.ACTIVE.name());
@@ -370,7 +362,7 @@
dataContextOfAction.getCaseParametersEntity().setPaymentSize(newLoanPaymentSize);
caseParametersRepository.save(dataContextOfAction.getCaseParametersEntity());
- return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getCommand().getCreatedOn());
}
@Transactional
@@ -447,8 +439,6 @@
command.getCommand().getPaymentSize(),
DateConverter.fromIsoString(command.getCommand().getCreatedOn()).toLocalDate(), runningBalances);
- final LocalDateTime today = today();
-
final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
designatorToAccountIdentifierMapper,
command.getCommand().getNote(),
@@ -467,7 +457,7 @@
//TODO: Should this be more sophisticated? Take into account what the payment amount was?
markCaseNotLate(dataContextOfAction);
- return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getCommand().getCreatedOn());
}
@Transactional
@@ -590,8 +580,6 @@
command.getCommand().getPaymentSize(),
DateConverter.fromIsoString(command.getCommand().getCreatedOn()).toLocalDate(), runningBalances);
- final LocalDateTime today = today();
-
final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
designatorToAccountIdentifierMapper,
command.getCommand().getNote(),
@@ -610,7 +598,7 @@
customerCase.setCurrentState(Case.State.CLOSED.name());
caseRepository.save(customerCase);
- return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getCommand().getCreatedOn());
}
@Transactional
@@ -634,8 +622,6 @@
final PaymentBuilder paymentBuilder =
closePaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, CostComponentService.today(), runningBalances);
- final LocalDateTime today = today();
-
final Optional<String> transactionIdentifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
designatorToAccountIdentifierMapper,
command.getCommand().getNote(),
@@ -647,7 +633,7 @@
customerCase.setCurrentState(Case.State.CLOSED.name());
caseRepository.save(customerCase);
- return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getCommand().getCreatedOn());
}
@Transactional
@@ -671,8 +657,6 @@
final PaymentBuilder paymentBuilder =
recoverPaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, CostComponentService.today(), runningBalances);
- final LocalDateTime today = today();
-
final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
designatorToAccountIdentifierMapper,
command.getCommand().getNote(),
@@ -691,7 +675,7 @@
customerCase.setCurrentState(Case.State.CLOSED.name());
caseRepository.save(customerCase);
- return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getCommand().getCreatedOn());
}
private Map<String, BigDecimal> getRequestedChargeAmounts(final @Nullable List<CostComponent> costComponents) {
@@ -746,8 +730,4 @@
final DataContextOfAction dataContextOfAction) {
lateCaseRepository.deleteByCaseId(dataContextOfAction.getCustomerCaseEntity().getId());
}
-
- private static LocalDateTime today() {
- return LocalDate.now(Clock.systemUTC()).atStartOfDay();
- }
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java
index d55d9f3..580293f 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RunningBalances.java
@@ -50,7 +50,7 @@
this.put(AccountDesignators.LATE_FEE_ACCRUAL, positive);
this.put(AccountDesignators.PRODUCT_LOSS_ALLOWANCE, negative);
this.put(AccountDesignators.GENERAL_LOSS_ALLOWANCE, negative);
- this.put(AccountDesignators.GENERAL_EXPENSE, negative);
+ this.put(AccountDesignators.EXPENSE, negative);
this.put(AccountDesignators.ENTRY, positive);
//TODO: derive signs from IndividualLendingPatternFactory.individualLendingRequiredAccounts instead.
}};
@@ -99,6 +99,7 @@
default BigDecimal getMaxCredit(final String accountDesignator, final BigDecimal amount) {
if (accountDesignator.equals(AccountDesignators.ENTRY) ||
+ accountDesignator.equals(AccountDesignators.EXPENSE) ||
accountDesignator.equals(AccountDesignators.PRODUCT_LOSS_ALLOWANCE))
return amount;
//entry account can achieve a "relative" negative balance, and
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderService.java
index 3dab27b..c51a300 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderService.java
@@ -15,13 +15,14 @@
*/
package io.mifos.individuallending.internal.service.costcomponent;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.internal.repository.CaseParametersEntity;
import io.mifos.individuallending.internal.service.DataContextOfAction;
import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
-import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
-import org.springframework.beans.factory.annotation.Autowired;
+import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import org.springframework.stereotype.Service;
import javax.annotation.Nonnull;
@@ -30,19 +31,16 @@
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
+
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.WRITE_OFF_ID;
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.WRITE_OFF_NAME;
/**
* @author Myrle Krantz
*/
@Service
public class WriteOffPaymentBuilderService implements PaymentBuilderService {
- private final ScheduledChargesService scheduledChargesService;
-
- @Autowired
- public WriteOffPaymentBuilderService(final ScheduledChargesService scheduledChargesService) {
- this.scheduledChargesService = scheduledChargesService;
- }
-
@Override
public PaymentBuilder getPaymentBuilder(
final @Nonnull DataContextOfAction dataContextOfAction,
@@ -51,11 +49,10 @@
final RunningBalances runningBalances)
{
final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
- final String productIdentifier = dataContextOfAction.getProductEntity().getIdentifier();
final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
- final List<ScheduledAction> scheduledActions = Collections.singletonList(new ScheduledAction(Action.WRITE_OFF, forDate));
- final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
- productIdentifier, scheduledActions);
+
+ final List<ScheduledCharge> scheduledCharges
+ = Collections.singletonList(getScheduledChargeForWriteOff(forDate));
final BigDecimal loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize();
@@ -70,4 +67,22 @@
minorCurrencyUnitDigits,
true);
}
+
+
+ private ScheduledCharge getScheduledChargeForWriteOff(final LocalDate forDate) {
+
+ final ChargeDefinition chargeDefinition = new ChargeDefinition();
+ chargeDefinition.setChargeAction(Action.WRITE_OFF.name());
+ chargeDefinition.setIdentifier(WRITE_OFF_ID);
+ chargeDefinition.setName(WRITE_OFF_NAME);
+ chargeDefinition.setDescription(WRITE_OFF_NAME);
+ chargeDefinition.setFromAccountDesignator(AccountDesignators.EXPENSE);
+ chargeDefinition.setToAccountDesignator(AccountDesignators.GENERAL_LOSS_ALLOWANCE);
+ chargeDefinition.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_DESIGNATOR.getValue());
+ chargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
+ chargeDefinition.setAmount(BigDecimal.valueOf(100));
+ chargeDefinition.setReadOnly(true);
+ final ScheduledAction scheduledAction = new ScheduledAction(Action.WRITE_OFF, forDate);
+ return new ScheduledCharge(scheduledAction, chargeDefinition, Optional.empty());
+ }
}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/schedule/LossProvisionChargesService.java b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/LossProvisionChargesService.java
index 6265bb5..a06707c 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/schedule/LossProvisionChargesService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/schedule/LossProvisionChargesService.java
@@ -29,8 +29,7 @@
import java.time.LocalDate;
import java.util.Optional;
-import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.PROVISION_FOR_LOSSES_ID;
-import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.PROVISION_FOR_LOSSES_NAME;
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
/**
* @author Myrle Krantz
@@ -88,14 +87,12 @@
final BigDecimal percentProvision,
final Action action) {
final ChargeDefinition ret = new ChargeDefinition();
- ret.setChargeAction(Action.WRITE_OFF.name());
+ ret.setChargeAction(action.name());
ret.setIdentifier(PROVISION_FOR_LOSSES_ID);
ret.setName(PROVISION_FOR_LOSSES_NAME);
ret.setDescription(PROVISION_FOR_LOSSES_NAME);
ret.setFromAccountDesignator(AccountDesignators.PRODUCT_LOSS_ALLOWANCE);
- ret.setAccrualAccountDesignator(AccountDesignators.GENERAL_LOSS_ALLOWANCE);
- ret.setAccrueAction(action.name());
- ret.setToAccountDesignator(AccountDesignators.GENERAL_EXPENSE);
+ ret.setToAccountDesignator(AccountDesignators.GENERAL_LOSS_ALLOWANCE);
ret.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_DESIGNATOR.getValue());
ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
ret.setAmount(percentProvision.negate());
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
index fba4800..a56fecd 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/mapper/ChargeDefinitionMapper.java
@@ -77,7 +77,7 @@
ret.setFromAccountDesignator(from.getFromAccountDesignator());
ret.setAccrualAccountDesignator(from.getAccrualAccountDesignator());
ret.setToAccountDesignator(from.getToAccountDesignator());
- ret.setReadOnly(Optional.ofNullable(from.getReadOnly()).orElseGet(() -> readOnlyLegacyMapper(from.getIdentifier())));
+ ret.setReadOnly(Optional.ofNullable(from.getReadOnly()).orElse(false));
if (from.getSegmentSet() != null && from.getFromSegment() != null && from.getToSegment() != null) {
ret.setForSegmentSet(from.getSegmentSet());
ret.setFromSegment(from.getFromSegment());
@@ -88,29 +88,6 @@
return ret;
}
- private static Boolean readOnlyLegacyMapper(final String identifier) {
- switch (identifier) {
- case INTEREST_ID:
- return false;
- case ALLOW_FOR_WRITE_OFF_ID:
- return false;
- case LATE_FEE_ID:
- return true;
- case DISBURSEMENT_FEE_ID:
- return false;
- case DISBURSE_PAYMENT_ID:
- return false;
- case LOAN_ORIGINATION_FEE_ID:
- return true;
- case PROCESSING_FEE_ID:
- return true;
- case REPAY_PRINCIPAL_ID:
- return false;
- default:
- return false;
- }
- }
-
private static String proportionalToLegacyMapper(final ChargeDefinitionEntity from,
final ChargeDefinition.ChargeMethod chargeMethod,
final String identifier) {
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java
new file mode 100644
index 0000000..3dd53e1
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 Kuelap, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.mifos.individuallending.internal.service.costcomponent;
+
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
+import io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.portfolio.api.v1.domain.CostComponent;
+import io.mifos.portfolio.api.v1.domain.Payment;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author Myrle Krantz
+ */
+@RunWith(Parameterized.class)
+public class WriteOffPaymentBuilderServiceTest {
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<PaymentBuilderServiceTestCase> ret = new ArrayList<>();
+ ret.add(simpleCase());
+ //TODO: add use case for when the general loss allowance account doesn't have enough to cover the write off.
+ return ret;
+ }
+
+ private static PaymentBuilderServiceTestCase simpleCase() {
+ final PaymentBuilderServiceTestCase ret = new PaymentBuilderServiceTestCase("simple case");
+ ret.runningBalances.adjustBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, ret.balance.negate());
+ ret.runningBalances.adjustBalance(AccountDesignators.GENERAL_LOSS_ALLOWANCE, ret.balance.negate());
+ return ret;
+ }
+
+ private final PaymentBuilderServiceTestCase testCase;
+
+ public WriteOffPaymentBuilderServiceTest(final PaymentBuilderServiceTestCase testCase) {
+ this.testCase = testCase;
+ }
+
+ @Test
+ public void getPaymentBuilder() throws Exception {
+ final PaymentBuilder paymentBuilder = PaymentBuilderServiceTestHarness.constructCallToPaymentBuilder(
+ (scheduledChargesService) -> new WriteOffPaymentBuilderService(), testCase);
+
+ final Payment payment = paymentBuilder.buildPayment(Action.WRITE_OFF, Collections.emptySet(), testCase.forDate.toLocalDate());
+ Assert.assertNotNull(payment);
+ final Map<String, CostComponent> mappedCostComponents = payment.getCostComponents().stream()
+ .collect(Collectors.toMap(CostComponent::getChargeIdentifier, x -> x));
+
+ Assert.assertEquals(
+ testCase.balance,
+ mappedCostComponents.get(ChargeIdentifiers.WRITE_OFF_ID).getAmount());
+ }
+
+}