Merge pull request #14 from myrlen/arrearsAndWriteOff
Arrears and write off
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/api/src/main/java/io/mifos/individuallending/api/v1/domain/workflow/Action.java b/api/src/main/java/io/mifos/individuallending/api/v1/domain/workflow/Action.java
index cd51961..1fc3dc7 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/domain/workflow/Action.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/domain/workflow/Action.java
@@ -30,6 +30,7 @@
APPLY_INTEREST("INTR"),
ACCEPT_PAYMENT("PPAY"),
MARK_LATE("ICCT"),
+ MARK_IN_ARREARS("ICCT"),
WRITE_OFF("ICCT"),
CLOSE("ICCT"),
RECOVER("ICCT");
diff --git a/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanEventConstants.java b/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanEventConstants.java
index 6264e74..219a016 100644
--- a/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanEventConstants.java
+++ b/api/src/main/java/io/mifos/individuallending/api/v1/events/IndividualLoanEventConstants.java
@@ -31,6 +31,7 @@
String ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE = "accept-payment-individualloan-case";
String CHECK_LATE_INDIVIDUALLOAN_CASE = "check-late-individualloan-case";
String MARK_LATE_INDIVIDUALLOAN_CASE = "mark-late-individualloan-case";
+ String MARK_IN_ARREARS_INDIVIDUALLOAN_CASE = "mark-in-arrears-individualloan-case";
String WRITE_OFF_INDIVIDUALLOAN_CASE = "write-off-individualloan-case";
String CLOSE_INDIVIDUALLOAN_CASE = "close-individualloan-case";
String RECOVER_INDIVIDUALLOAN_CASE = "recover-individualloan-case";
@@ -44,6 +45,7 @@
String SELECTOR_ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE + "'";
String SELECTOR_CHECK_LATE_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + CHECK_LATE_INDIVIDUALLOAN_CASE + "'";
String SELECTOR_MARK_LATE_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + MARK_LATE_INDIVIDUALLOAN_CASE + "'";
+ String SELECTOR_MARK_IN_ARREARS_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + MARK_IN_ARREARS_INDIVIDUALLOAN_CASE + "'";
String SELECTOR_WRITE_OFF_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + WRITE_OFF_INDIVIDUALLOAN_CASE + "'";
String SELECTOR_CLOSE_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + CLOSE_INDIVIDUALLOAN_CASE + "'";
String SELECTOR_RECOVER_INDIVIDUALLOAN_CASE = SELECTOR_NAME + " = '" + RECOVER_INDIVIDUALLOAN_CASE + "'";
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/AccountingFixture.java b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
index 892a27d..13ecedd 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AccountingFixture.java
@@ -245,6 +245,7 @@
private static Account productLossAllowanceAccount() {
final Account ret = new Account();
ret.setIdentifier(PRODUCT_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER);
+ ret.setLedger(CUSTOMER_LOAN_LEDGER_IDENTIFIER);
ret.setType(AccountType.ASSET.name());
return ret;
}
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 63f71b3..9ac056b 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -23,9 +23,7 @@
import io.mifos.core.lang.DateConverter;
import io.mifos.individuallending.api.v1.domain.caseinstance.CaseParameters;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
-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.product.ChargeProportionalDesignator;
+import io.mifos.individuallending.api.v1.domain.product.*;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
@@ -41,14 +39,15 @@
import org.junit.Before;
import org.junit.Test;
+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.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
+import java.util.stream.Stream;
import static io.mifos.portfolio.Fixture.MINOR_CURRENCY_UNIT_DIGITS;
@@ -78,6 +77,7 @@
private BigDecimal interestAccrued = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
private BigDecimal nonLateFees = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
private BigDecimal lateFees = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
+ private BigDecimal productLossAllowance = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
@Before
@@ -235,14 +235,12 @@
UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
int week = 0;
- final List<Payment> payments = new ArrayList<>();
while (expectedCurrentPrincipal.compareTo(BigDecimal.ZERO) > 0) {
logger.info("Simulating week {}. Expected current principal {}.", week, expectedCurrentPrincipal);
step6CalculateInterestAndCheckForLatenessForWeek(today, week);
final BigDecimal interestAccruedBeforePayment = interestAccrued;
final BigDecimal nextRepaymentAmount = findNextRepaymentAmount(today.plusDays((week+1)*7));
final Payment payment = step7PaybackPartialAmount(nextRepaymentAmount, today.plusDays((week + 1) * 7), BigDecimal.ZERO);
- payments.add(payment);
final BigDecimal interestAccrual = payment.getBalanceAdjustments().remove(AccountDesignators.INTEREST_ACCRUAL); //Don't compare these with planned payment.
final BigDecimal customerLoanInterest = payment.getBalanceAdjustments().remove(AccountDesignators.CUSTOMER_LOAN_INTEREST);
Assert.assertEquals("week " + week, interestAccrual.negate(), customerLoanInterest);
@@ -284,7 +282,7 @@
today,
(week * 7) + 1,
(week + 1) * 7 + 2,
- 8,
+ 7,
lateFee);
final BigDecimal nextRepaymentAmount = findNextRepaymentAmount(today.plusDays((week + 1) * 7 + 2));
step7PaybackPartialAmount(nextRepaymentAmount, today.plusDays((week + 1) * 7 + 2), lateFee);
@@ -304,6 +302,38 @@
step8Close(DateConverter.fromIsoString(plannedPayments.get(plannedPayments.size()-1).getPayment().getDate()));
}
+ @Test
+ public void workflowTerminatingInWriteOff() throws InterruptedException {
+ final LocalDateTime today = midnightToday();
+
+ step1CreateProduct();
+ step2CreateCase();
+ step3OpenCase(today);
+ step4ApproveCase(today);
+
+ step5Disburse(
+ BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
+ today,
+ UPPER_RANGE_DISBURSEMENT_FEE_ID, BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS));
+
+ final BigDecimal lateFee = BigDecimal.valueOf(15_36, MINOR_CURRENCY_UNIT_DIGITS); //??? TODO: check the late fee value.
+ step6CalculateInterestAndCheckForLatenessForRangeOfDays(
+ today,
+ 1,
+ 8,
+ 7,
+ lateFee);
+ step6ICalculateInterestAndLossAllowancesForLateLoanForRangeOfDays(
+ today,
+ new LossProvisionStep(0, BigDecimal.valueOf(1)),
+ new LossProvisionStep(1, BigDecimal.valueOf(9)),
+ new LossProvisionStep(30, BigDecimal.valueOf(30)),
+ new LossProvisionStep(60, BigDecimal.valueOf(60))
+ );
+
+ step8IWriteOff(today.plusDays(68));
+ }
+
private BigDecimal findNextRepaymentAmount(
final LocalDateTime forDateTime) {
final Payment nextPayment = portfolioManager.getCostComponentsForAction(
@@ -371,6 +401,15 @@
portfolioManager.enableProduct(product.getIdentifier(), true);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_PRODUCT_ENABLE, product.getIdentifier()));
+
+ final List<LossProvisionStep> lossProvisionSteps = Arrays.asList(
+ new LossProvisionStep(0, BigDecimal.ONE),
+ new LossProvisionStep(1, BigDecimal.valueOf(9)),
+ new LossProvisionStep(30, BigDecimal.valueOf(30)),
+ new LossProvisionStep(60, BigDecimal.valueOf(60)));
+ final LossProvisionConfiguration lossProvisionConfiguration = new LossProvisionConfiguration(lossProvisionSteps);
+ individualLending.changeLossProvisionConfiguration(product.getIdentifier(), lossProvisionConfiguration);
+ Assert.assertTrue(this.eventRecorder.wait(IndividualLoanEventConstants.PUT_LOSS_PROVISION_STEPS, product.getIdentifier()));
}
private void step2CreateCase() throws InterruptedException {
@@ -399,6 +438,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.OPEN,
+ forDateTime,
Collections.singletonList(assignEntryToTeller()),
IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE,
Case.State.PENDING);
@@ -421,6 +461,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DENY,
+ forDateTime,
Collections.singletonList(assignEntryToTeller()),
IndividualLoanEventConstants.DENY_INDIVIDUALLOAN_CASE,
Case.State.CLOSED);
@@ -447,6 +488,7 @@
product.getIdentifier(),
customerCase.getIdentifier(),
Action.APPROVE,
+ forDateTime,
Collections.singletonList(assignEntryToTeller()),
IndividualLoanEventConstants.APPROVE_INDIVIDUALLOAN_CASE,
Case.State.APPROVED);
@@ -478,6 +520,7 @@
final String whichDisbursementFee,
final BigDecimal disbursementFeeAmount) throws InterruptedException {
logger.info("step5Disburse '{}'", amount);
+ final BigDecimal provisionForLosses = amount.multiply(BigDecimal.valueOf(0.01)).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
@@ -494,30 +537,32 @@
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.WRITE_OFF, Action.CLOSE);
+ Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.MARK_IN_ARREARS, Action.WRITE_OFF, Action.CLOSE);
final Set<Debtor> debtors = new HashSet<>();
debtors.add(new Debtor(customerLoanPrincipalIdentifier, amount.toPlainString()));
debtors.add(new Debtor(customerLoanFeeIdentifier, PROCESSING_FEE_AMOUNT.add(disbursementFeeAmount).add(LOAN_ORIGINATION_FEE_AMOUNT).toPlainString()));
+ debtors.add(new Debtor(AccountingFixture.GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER, provisionForLosses.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
creditors.add(new Creditor(AccountingFixture.TELLER_ONE_ACCOUNT_IDENTIFIER, amount.toString()));
creditors.add(new Creditor(AccountingFixture.PROCESSING_FEE_INCOME_ACCOUNT_IDENTIFIER, PROCESSING_FEE_AMOUNT.toPlainString()));
creditors.add(new Creditor(AccountingFixture.DISBURSEMENT_FEE_INCOME_ACCOUNT_IDENTIFIER, disbursementFeeAmount.toPlainString()));
creditors.add(new Creditor(AccountingFixture.LOAN_ORIGINATION_FEES_ACCOUNT_IDENTIFIER, LOAN_ORIGINATION_FEE_AMOUNT.toPlainString()));
+ creditors.add(new Creditor(AccountingFixture.PRODUCT_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER, provisionForLosses.toPlainString()));
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE);
expectedCurrentPrincipal = expectedCurrentPrincipal.add(amount);
interestAccrued = BigDecimal.ZERO;
nonLateFees = nonLateFees.add(disbursementFeeAmount).add(PROCESSING_FEE_AMOUNT).add(LOAN_ORIGINATION_FEE_AMOUNT);
lateFees = BigDecimal.ZERO;
+ productLossAllowance = provisionForLosses;
updateBalanceMock();
}
@@ -581,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(
@@ -602,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);
@@ -615,6 +667,10 @@
Assert.assertTrue(eventRecorder.wait(IndividualLoanEventConstants.APPLY_INTEREST_INDIVIDUALLOAN_CASE,
new IndividualLoanCommandEvent(product.getIdentifier(), customerCase.getIdentifier(), midnightTimeStamp)));
+ if (calculatedLateFee.compareTo(BigDecimal.ZERO) != 0)
+ Assert.assertTrue(eventRecorder.wait(IndividualLoanEventConstants.MARK_LATE_INDIVIDUALLOAN_CASE,
+ new IndividualLoanCommandEvent(product.getIdentifier(), customerCase.getIdentifier(), midnightTimeStamp)));
+
final Case customerCaseAfterStateChange = portfolioManager.getCase(product.getIdentifier(), customerCase.getIdentifier());
Assert.assertEquals(customerCaseAfterStateChange.getCurrentState(), Case.State.ACTIVE.name());
@@ -641,11 +697,17 @@
lateFeeDebtors.add(new Debtor(
customerLoanFeeIdentifier,
calculatedLateFee.toPlainString()));
+ lateFeeDebtors.add(new Debtor(
+ AccountingFixture.GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER,
+ provisionForLosses.toPlainString()));
final Set<Creditor> lateFeeCreditors = new HashSet<>();
lateFeeCreditors.add(new Creditor(
AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER,
calculatedLateFee.toPlainString()));
+ lateFeeCreditors.add(new Creditor(
+ AccountingFixture.PRODUCT_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER,
+ provisionForLosses.toPlainString()));
AccountingFixture.verifyTransfer(
ledgerManager,
lateFeeDebtors,
@@ -654,6 +716,7 @@
customerCase.getIdentifier(),
Action.MARK_LATE);
lateFees = lateFees.add(calculatedLateFee);
+ productLossAllowance = productLossAllowance.add(provisionForLosses);
}
interestAccrued = interestAccrued.add(calculatedInterest);
@@ -661,6 +724,144 @@
logger.info("Completed step6CalculateInterestAccrualAndCheckForLateness");
}
+ private void step6ICalculateInterestAndLossAllowancesForLateLoanForRangeOfDays(
+ final LocalDateTime referenceDate,
+ final LossProvisionStep... lossProvisionSteps) throws InterruptedException
+ {
+ try {
+ final Map<Integer, BigDecimal> lossProvisionConfiguration = Stream.of(lossProvisionSteps)
+ .collect(Collectors.toMap(LossProvisionStep::getDaysLate, LossProvisionStep::getPercentProvision));
+
+ IntStream.rangeClosed(9, 67)
+ .forEach(day -> {
+ try {
+ final int daysLate = day - 7;
+ step6ICalculateInterestAndLossAllowancesForLateLoan(
+ referenceDate.plusDays(day),
+ daysLate,
+ lossProvisionConfiguration.get(daysLate));
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+ catch (RuntimeException e) {
+ final Throwable cause = e.getCause();
+ if (cause != null && cause.getClass().isAssignableFrom(InterruptedException.class))
+ throw (InterruptedException)e.getCause();
+ else
+ throw e;
+ }
+ }
+
+ private void step6ICalculateInterestAndLossAllowancesForLateLoan(
+ final LocalDateTime forDateTime,
+ final int daysLate,
+ final @Nullable BigDecimal percentProvision) throws InterruptedException
+ {
+ logger.info("step6ICalculateInterestAndLossAllowancesForLateLoan '{}'", forDateTime);
+ final String beatIdentifier = "alignment0";
+ final String midnightTimeStamp = DateConverter.toIsoString(forDateTime);
+
+ final BigDecimal dailyInterestRate = Fixture.INTEREST_RATE
+ .divide(BigDecimal.valueOf(100), 8, BigDecimal.ROUND_HALF_EVEN)
+ .divide(Fixture.ACCRUAL_PERIODS, 8, BigDecimal.ROUND_HALF_EVEN);
+
+ final BigDecimal calculatedInterest = expectedCurrentPrincipal
+ .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(
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ Action.APPLY_INTEREST,
+ 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,
+ new BeatPublishEvent(EventConstants.DESTINATION, beatIdentifier, midnightTimeStamp)));
+
+ Assert.assertTrue(this.eventRecorder.wait(IndividualLoanEventConstants.CHECK_LATE_INDIVIDUALLOAN_CASE,
+ new IndividualLoanCommandEvent(product.getIdentifier(), customerCase.getIdentifier(), midnightTimeStamp)));
+
+ Assert.assertTrue(eventRecorder.wait(IndividualLoanEventConstants.APPLY_INTEREST_INDIVIDUALLOAN_CASE,
+ new IndividualLoanCommandEvent(product.getIdentifier(), customerCase.getIdentifier(), midnightTimeStamp)));
+
+ if (percentProvision != null) {
+ Assert.assertTrue(eventRecorder.wait(IndividualLoanEventConstants.MARK_IN_ARREARS_INDIVIDUALLOAN_CASE,
+ new IndividualLoanCommandEvent(product.getIdentifier(), customerCase.getIdentifier(), midnightTimeStamp)));
+ }
+
+
+ final Case customerCaseAfterStateChange = portfolioManager.getCase(product.getIdentifier(), customerCase.getIdentifier());
+ Assert.assertEquals(customerCaseAfterStateChange.getCurrentState(), Case.State.ACTIVE.name());
+
+ final Set<Debtor> debtors = new HashSet<>();
+ debtors.add(new Debtor(
+ customerLoanInterestIdentifier,
+ calculatedInterest.toPlainString()));
+
+ final Set<Creditor> creditors = new HashSet<>();
+ creditors.add(new Creditor(
+ AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER,
+ calculatedInterest.toPlainString()));
+ AccountingFixture.verifyTransfer(
+ ledgerManager,
+ debtors,
+ creditors,
+ product.getIdentifier(),
+ customerCase.getIdentifier(), Action.APPLY_INTEREST);
+
+ if (percentProvision != null) {
+ final Set<Debtor> lateFeeDebtors = new HashSet<>();
+ lateFeeDebtors.add(new Debtor(
+ AccountingFixture.GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER,
+ provisionForLosses.toPlainString()));
+
+ final Set<Creditor> lateFeeCreditors = new HashSet<>();
+ lateFeeCreditors.add(new Creditor(
+ AccountingFixture.PRODUCT_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER,
+ provisionForLosses.toPlainString()));
+ AccountingFixture.verifyTransfer(
+ ledgerManager,
+ lateFeeDebtors,
+ lateFeeCreditors,
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ Action.MARK_IN_ARREARS);
+ productLossAllowance = productLossAllowance.add(provisionForLosses);
+ }
+ interestAccrued = interestAccrued.add(calculatedInterest);
+
+ updateBalanceMock();
+ logger.info("Completed step6ICalculateInterestAndLossAllowancesForLateLoan");
+
+ }
+
private Payment step7PaybackPartialAmount(
final BigDecimal amount,
final LocalDateTime forDateTime,
@@ -689,10 +890,9 @@
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.WRITE_OFF, Action.CLOSE);
+ Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.MARK_IN_ARREARS, Action.WRITE_OFF, Action.CLOSE);
final Set<Debtor> debtors = new HashSet<>();
BigDecimal tellerOneDebit = principal;
@@ -736,7 +936,7 @@
private void step8Close(
final LocalDateTime forDateTime) throws InterruptedException
{
- logger.info("step8Close");
+ logger.info("step8Close for '{}'", forDateTime);
checkCostComponentForActionCorrect(
product.getIdentifier(),
@@ -750,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.
@@ -757,6 +958,47 @@
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
}
+ private void step8IWriteOff(
+ final LocalDateTime forDateTime) throws InterruptedException {
+ logger.info("step8IWriteOff for '{}'", forDateTime);
+
+ checkCostComponentForActionCorrect(
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ Action.WRITE_OFF,
+ null,
+ null,
+ forDateTime,
+ MINOR_CURRENCY_UNIT_DIGITS,
+ new CostComponent(ChargeIdentifiers.WRITE_OFF_ID, expectedCurrentPrincipal),
+ new CostComponent(ChargeIdentifiers.INTEREST_ID, interestAccrued),
+ new CostComponent(ChargeIdentifiers.LATE_FEE_ID, lateFees));
+ checkStateTransfer(
+ product.getIdentifier(),
+ customerCase.getIdentifier(),
+ Action.WRITE_OFF,
+ 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()));
+ debtors.add(new Debtor(AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER, lateFees.toPlainString()));
+ 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()));
+ 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() {
logger.info("Updating balance mocks");
final BigDecimal allFees = lateFees.add(nonLateFees);
@@ -765,9 +1007,12 @@
AccountingFixture.mockBalance(customerLoanInterestIdentifier, interestAccrued);
AccountingFixture.mockBalance(AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER, interestAccrued);
AccountingFixture.mockBalance(AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER, lateFees);
+ AccountingFixture.mockBalance(AccountingFixture.PRODUCT_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER, productLossAllowance.negate());
+ AccountingFixture.mockBalance(AccountingFixture.GENERAL_LOSS_ALLOWANCE_ACCOUNT_IDENTIFIER, productLossAllowance);
logger.info("updated currentPrincipal '{}'", expectedCurrentPrincipal);
logger.info("updated interestAccrued '{}'", interestAccrued);
logger.info("updated nonLateFees '{}'", nonLateFees);
logger.info("updated lateFees '{}'", lateFees);
+ logger.info("updated productLossAllowance '{}'", productLossAllowance);
}
}
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
index 541f490..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.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/component-test/src/main/java/io/mifos/portfolio/listener/IndividualLoanCaseCommandEventListener.java b/component-test/src/main/java/io/mifos/portfolio/listener/IndividualLoanCaseCommandEventListener.java
index e198db2..7107590 100644
--- a/component-test/src/main/java/io/mifos/portfolio/listener/IndividualLoanCaseCommandEventListener.java
+++ b/component-test/src/main/java/io/mifos/portfolio/listener/IndividualLoanCaseCommandEventListener.java
@@ -119,6 +119,16 @@
}
@JmsListener(
+ subscription = IndividualLoanEventConstants.DESTINATION,
+ destination = IndividualLoanEventConstants.DESTINATION,
+ selector = IndividualLoanEventConstants.SELECTOR_MARK_IN_ARREARS_INDIVIDUALLOAN_CASE
+ )
+ public void onMarkInArrears(@Header(TenantHeaderFilter.TENANT_HEADER) final String tenant,
+ final String payload) {
+ this.eventRecorder.event(tenant, IndividualLoanEventConstants.MARK_IN_ARREARS_INDIVIDUALLOAN_CASE, payload, IndividualLoanCommandEvent.class);
+ }
+
+ @JmsListener(
subscription = IndividualLoanEventConstants.DESTINATION,
destination = IndividualLoanEventConstants.DESTINATION,
selector = IndividualLoanEventConstants.SELECTOR_WRITE_OFF_INDIVIDUALLOAN_CASE
diff --git a/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java b/service/src/main/java/io/mifos/individuallending/IndividualLendingPatternFactory.java
index 59c2f47..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,
@@ -129,6 +129,7 @@
private final AcceptPaymentBuilderService acceptPaymentBuilderService;
private final ClosePaymentBuilderService closePaymentBuilderService;
private final MarkLatePaymentBuilderService markLatePaymentBuilderService;
+ private final MarkInArrearsPaymentBuilderService markInArrearsBuilderService;
private final WriteOffPaymentBuilderService writeOffPaymentBuilderService;
private final RecoverPaymentBuilderService recoverPaymentBuilderService;
private final AccountingAdapter accountingAdapter;
@@ -148,7 +149,7 @@
final AcceptPaymentBuilderService acceptPaymentBuilderService,
final ClosePaymentBuilderService closePaymentBuilderService,
final MarkLatePaymentBuilderService markLatePaymentBuilderService,
- final WriteOffPaymentBuilderService writeOffPaymentBuilderService,
+ MarkInArrearsPaymentBuilderService markInArrearsBuilderService, final WriteOffPaymentBuilderService writeOffPaymentBuilderService,
final RecoverPaymentBuilderService recoverPaymentBuilderService,
AccountingAdapter accountingAdapter, final CustomerManager customerManager,
final IndividualLendingCommandDispatcher individualLendingCommandDispatcher,
@@ -164,6 +165,7 @@
this.acceptPaymentBuilderService = acceptPaymentBuilderService;
this.closePaymentBuilderService = closePaymentBuilderService;
this.markLatePaymentBuilderService = markLatePaymentBuilderService;
+ this.markInArrearsBuilderService = markInArrearsBuilderService;
this.writeOffPaymentBuilderService = writeOffPaymentBuilderService;
this.recoverPaymentBuilderService = recoverPaymentBuilderService;
this.accountingAdapter = accountingAdapter;
@@ -329,6 +331,9 @@
case MARK_LATE:
paymentBuilderService = markLatePaymentBuilderService;
break;
+ case MARK_IN_ARREARS:
+ paymentBuilderService = markInArrearsBuilderService;
+ break;
case WRITE_OFF:
paymentBuilderService = writeOffPaymentBuilderService;
break;
@@ -368,7 +373,7 @@
case APPROVED:
return new HashSet<>(Arrays.asList(Action.DISBURSE, Action.CLOSE));
case ACTIVE:
- return new HashSet<>(Arrays.asList(Action.CLOSE, Action.ACCEPT_PAYMENT, Action.MARK_LATE, Action.APPLY_INTEREST, Action.DISBURSE, Action.WRITE_OFF));
+ return new HashSet<>(Arrays.asList(Action.CLOSE, Action.ACCEPT_PAYMENT, Action.MARK_LATE, Action.APPLY_INTEREST, Action.DISBURSE, Action.MARK_IN_ARREARS, Action.WRITE_OFF));
case CLOSED:
return Collections.emptySet();
default:
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/MarkInArrearsCommand.java b/service/src/main/java/io/mifos/individuallending/internal/command/MarkInArrearsCommand.java
new file mode 100644
index 0000000..86dbf22
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/MarkInArrearsCommand.java
@@ -0,0 +1,59 @@
+/*
+ * 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.command;
+
+/**
+ * @author Myrle Krantz
+ */
+public class MarkInArrearsCommand {
+ private final String productIdentifier;
+ private final String caseIdentifier;
+ private final String forTime;
+ private final long daysLate;
+
+ public MarkInArrearsCommand(String productIdentifier, String caseIdentifier, String forTime, long daysLate) {
+ this.productIdentifier = productIdentifier;
+ this.caseIdentifier = caseIdentifier;
+ this.forTime = forTime;
+ this.daysLate = daysLate;
+ }
+
+ public String getProductIdentifier() {
+ return productIdentifier;
+ }
+
+ public String getCaseIdentifier() {
+ return caseIdentifier;
+ }
+
+ public String getForTime() {
+ return forTime;
+ }
+
+ public long getDaysLate() {
+ return daysLate;
+ }
+
+ @Override
+ public String toString() {
+ return "MarkInArrearsCommand{" +
+ "productIdentifier='" + productIdentifier + '\'' +
+ ", caseIdentifier='" + caseIdentifier + '\'' +
+ ", forTime='" + forTime + '\'' +
+ ", daysLate=" + daysLate +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
index b476d47..ef3812d 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/BeatPublishCommandHandler.java
@@ -29,9 +29,15 @@
import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
import io.mifos.individuallending.internal.command.ApplyInterestCommand;
import io.mifos.individuallending.internal.command.CheckLateCommand;
+import io.mifos.individuallending.internal.command.MarkInArrearsCommand;
import io.mifos.individuallending.internal.command.MarkLateCommand;
-import io.mifos.individuallending.internal.service.*;
+import io.mifos.individuallending.internal.repository.LateCaseEntity;
+import io.mifos.individuallending.internal.repository.LateCaseRepository;
+import io.mifos.individuallending.internal.repository.LossProvisionStepEntity;
+import io.mifos.individuallending.internal.repository.LossProvisionStepRepository;
import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.DataContextService;
+import io.mifos.individuallending.internal.service.costcomponent.RealRunningBalances;
import io.mifos.individuallending.internal.service.schedule.Period;
import io.mifos.individuallending.internal.service.schedule.ScheduledActionHelpers;
import io.mifos.portfolio.api.v1.domain.Case;
@@ -53,7 +59,9 @@
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
+import java.time.LocalDate;
import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -73,6 +81,8 @@
private final ApplicationName applicationName;
private final CommandBus commandBus;
private final AccountingAdapter accountingAdapter;
+ private final LateCaseRepository lateCaseRepository;
+ private final LossProvisionStepRepository lossProvisionStepRepository;
@Autowired
public BeatPublishCommandHandler(
@@ -82,7 +92,9 @@
final DataContextService dataContextService,
final ApplicationName applicationName,
final CommandBus commandBus,
- final AccountingAdapter accountingAdapter) {
+ final AccountingAdapter accountingAdapter,
+ final LateCaseRepository lateCaseRepository,
+ final LossProvisionStepRepository lossProvisionStepRepository) {
this.caseRepository = caseRepository;
this.caseCommandRepository = caseCommandRepository;
this.portfolioProperties = portfolioProperties;
@@ -90,6 +102,8 @@
this.applicationName = applicationName;
this.commandBus = commandBus;
this.accountingAdapter = accountingAdapter;
+ this.lateCaseRepository = lateCaseRepository;
+ this.lossProvisionStepRepository = lossProvisionStepRepository;
}
@Transactional
@@ -133,30 +147,26 @@
public IndividualLoanCommandEvent process(final CheckLateCommand command) {
final String productIdentifier = command.getProductIdentifier();
final String caseIdentifier = command.getCaseIdentifier();
- final LocalDateTime forTime = DateConverter.fromIsoString(command.getForTime());
+ final LocalDateTime forDateTime = DateConverter.fromIsoString(command.getForTime());
+ final LocalDate forDate = forDateTime.toLocalDate();
final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
productIdentifier, caseIdentifier, Collections.emptyList());
- final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
- = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
- final String customerLoanPrincipalAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
- final String customerLoanInterestAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_INTEREST);
- final String lateFeeAccrualAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.LATE_FEE_ACCRUAL);
+ final RealRunningBalances balances = new RealRunningBalances(accountingAdapter, dataContextOfAction);
- final BigDecimal currentBalance = accountingAdapter.getCurrentAccountBalance(customerLoanPrincipalAccountIdentifier);
+ final BigDecimal currentBalance = balances.getAccountBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
if (currentBalance.compareTo(BigDecimal.ZERO) == 0) //No late fees if the current balance is zilch.
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getForTime());
- final LocalDateTime dateOfMostRecentDisbursement =
- accountingAdapter.getDateOfMostRecentEntryContainingMessage(customerLoanPrincipalAccountIdentifier, dataContextOfAction.getMessageForCharge(Action.DISBURSE))
+ final LocalDateTime dateOfMostRecentDisbursement = dateOfMostRecentDisburse(dataContextOfAction.getCustomerCaseEntity().getId())
.orElseThrow(() ->
ServiceException.badRequest("No last disbursal date for ''{0}.{1}'' could be determined. " +
"Therefore it cannot be checked for lateness.", productIdentifier, caseIdentifier));
final List<Period> repaymentPeriods = ScheduledActionHelpers.generateRepaymentPeriods(
dateOfMostRecentDisbursement.toLocalDate(),
- forTime.toLocalDate(),
+ forDate,
dataContextOfAction.getCaseParameters())
.collect(Collectors.toList());
@@ -167,37 +177,59 @@
.getPaymentSize()
.multiply(BigDecimal.valueOf(repaymentPeriodsBetweenBeginningAndToday));
- final BigDecimal principalSum = accountingAdapter.sumMatchingEntriesSinceDate(
- customerLoanPrincipalAccountIdentifier,
- dateOfMostRecentDisbursement.toLocalDate(),
- dataContextOfAction.getMessageForCharge(Action.ACCEPT_PAYMENT));
- final BigDecimal interestSum = accountingAdapter.sumMatchingEntriesSinceDate(
- customerLoanInterestAccountIdentifier,
- dateOfMostRecentDisbursement.toLocalDate(),
- dataContextOfAction.getMessageForCharge(Action.ACCEPT_PAYMENT));
- final BigDecimal paymentsSum = principalSum.add(interestSum);
-
- final BigDecimal lateFeesAccrued = accountingAdapter.sumMatchingEntriesSinceDate(
- lateFeeAccrualAccountIdentifier,
- dateOfMostRecentDisbursement.toLocalDate(),
- dataContextOfAction.getMessageForCharge(Action.MARK_LATE));
+ final BigDecimal principalPaymentSum = balances.getSumOfChargesForActionSinceDate(
+ AccountDesignators.CUSTOMER_LOAN_PRINCIPAL,
+ Action.ACCEPT_PAYMENT,
+ dateOfMostRecentDisbursement);
+ final BigDecimal interestPaymentSum = balances.getSumOfChargesForActionSinceDate(
+ AccountDesignators.CUSTOMER_LOAN_INTEREST,
+ Action.ACCEPT_PAYMENT,
+ dateOfMostRecentDisbursement);
+ final BigDecimal feesPaymentSum = balances.getSumOfChargesForActionSinceDate(
+ AccountDesignators.CUSTOMER_LOAN_FEES,
+ Action.ACCEPT_PAYMENT,
+ dateOfMostRecentDisbursement);
+ final BigDecimal lateFeesSum = balances.getSumOfChargesForActionSinceDate(
+ AccountDesignators.LATE_FEE_INCOME,
+ Action.ACCEPT_PAYMENT,
+ dateOfMostRecentDisbursement);
+ final BigDecimal paymentsSum = principalPaymentSum.add(interestPaymentSum).add(feesPaymentSum.subtract(lateFeesSum));
if (paymentsSum.compareTo(expectedPaymentSum) < 0) {
- final Optional<LocalDateTime> dateOfMostRecentLateFee = dateOfMostRecentMarkLate(dataContextOfAction.getCustomerCaseEntity().getId());
- if (!dateOfMostRecentLateFee.isPresent() ||
- mostRecentLateFeeIsBeforeMostRecentRepaymentPeriod(repaymentPeriods, dateOfMostRecentLateFee.get())) {
+ final Optional<LocalDateTime> dateLateSince = dateLateSince(dataContextOfAction.getCustomerCaseEntity().getId());
+ if (!dateLateSince.isPresent()) {
commandBus.dispatch(new MarkLateCommand(productIdentifier, caseIdentifier, command.getForTime()));
}
+
+ if (dateLateSince.isPresent()) {
+ int daysLate;
+ try {
+ daysLate = Math.toIntExact(dateLateSince.get().until(forDateTime, ChronoUnit.DAYS)) + 1;
+ }
+ catch (ArithmeticException e) {
+ daysLate = -1;
+ }
+ if (daysLate > 1) {
+ final Optional<LossProvisionStepEntity> lossStepEntity = lossProvisionStepRepository.findByProductIdAndDaysLate(dataContextOfAction.getProductEntity().getId(), daysLate);
+ if (lossStepEntity.isPresent()) {
+ commandBus.dispatch(new MarkInArrearsCommand(productIdentifier, caseIdentifier, command.getForTime(), daysLate));
+ }
+ }
+ }
}
return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getForTime());
}
- private Optional<LocalDateTime> dateOfMostRecentMarkLate(final Long caseId) {
+ private Optional<LocalDateTime> dateLateSince(final Long caseId) {
+ return lateCaseRepository.findByCaseId(caseId).map(LateCaseEntity::getLateSince);
+ }
+
+ private Optional<LocalDateTime> dateOfMostRecentDisburse(final Long caseId) {
final Pageable pageRequest = new PageRequest(0, 10, Sort.Direction.DESC, "createdOn");
final Page<CaseCommandEntity> page = caseCommandRepository.findByCaseIdAndActionName(
caseId,
- Action.MARK_LATE.name(),
+ Action.DISBURSE.name(),
pageRequest);
return page.getContent().stream().findFirst().map(CaseCommandEntity::getCreatedOn);
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 23f313f..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
@@ -30,6 +30,8 @@
import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
import io.mifos.individuallending.internal.command.*;
import io.mifos.individuallending.internal.repository.CaseParametersRepository;
+import io.mifos.individuallending.internal.repository.LateCaseEntity;
+import io.mifos.individuallending.internal.repository.LateCaseRepository;
import io.mifos.individuallending.internal.service.DataContextOfAction;
import io.mifos.individuallending.internal.service.DataContextService;
import io.mifos.individuallending.internal.service.DesignatorToAccountIdentifierMapper;
@@ -46,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;
@@ -72,12 +72,14 @@
private final AcceptPaymentBuilderService acceptPaymentBuilderService;
private final ClosePaymentBuilderService closePaymentBuilderService;
private final MarkLatePaymentBuilderService markLatePaymentBuilderService;
+ private final MarkInArrearsPaymentBuilderService markInArrearsPaymentBuilderService;
private final WriteOffPaymentBuilderService writeOffPaymentBuilderService;
private final RecoverPaymentBuilderService recoverPaymentBuilderService;
private final AccountingAdapter accountingAdapter;
private final CaseCommandRepository caseCommandRepository;
private final TaskInstanceRepository taskInstanceRepository;
private final CaseParametersRepository caseParametersRepository;
+ private final LateCaseRepository lateCaseRepository;
@Autowired
public IndividualLoanCommandHandler(
@@ -91,11 +93,14 @@
final AcceptPaymentBuilderService acceptPaymentBuilderService,
final ClosePaymentBuilderService closePaymentBuilderService,
final MarkLatePaymentBuilderService markLatePaymentBuilderService,
+ final MarkInArrearsPaymentBuilderService markInArrearsPaymentBuilderService,
final WriteOffPaymentBuilderService writeOffPaymentBuilderService,
final RecoverPaymentBuilderService recoverPaymentBuilderService,
final AccountingAdapter accountingAdapter,
- CaseCommandRepository caseCommandRepository, final TaskInstanceRepository taskInstanceRepository,
- final CaseParametersRepository caseParametersRepository) {
+ final CaseCommandRepository caseCommandRepository,
+ final TaskInstanceRepository taskInstanceRepository,
+ final CaseParametersRepository caseParametersRepository,
+ final LateCaseRepository lateCaseRepository) {
this.caseRepository = caseRepository;
this.dataContextService = dataContextService;
this.openPaymentBuilderService = openPaymentBuilderService;
@@ -106,12 +111,14 @@
this.acceptPaymentBuilderService = acceptPaymentBuilderService;
this.closePaymentBuilderService = closePaymentBuilderService;
this.markLatePaymentBuilderService = markLatePaymentBuilderService;
+ this.markInArrearsPaymentBuilderService = markInArrearsPaymentBuilderService;
this.writeOffPaymentBuilderService = writeOffPaymentBuilderService;
this.recoverPaymentBuilderService = recoverPaymentBuilderService;
this.accountingAdapter = accountingAdapter;
this.caseCommandRepository = caseCommandRepository;
this.taskInstanceRepository = taskInstanceRepository;
this.caseParametersRepository = caseParametersRepository;
+ this.lateCaseRepository = lateCaseRepository;
}
@Transactional
@@ -137,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,
@@ -158,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
@@ -184,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,
@@ -204,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 {
@@ -279,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(),
@@ -300,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
@@ -326,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(),
@@ -346,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());
@@ -361,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
@@ -438,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(),
@@ -455,7 +454,10 @@
Action.ACCEPT_PAYMENT,
transactionUniqueifier);
- return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
+ //TODO: Should this be more sophisticated? Take into account what the payment amount was?
+ markCaseNotLate(dataContextOfAction);
+
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getCommand().getCreatedOn());
}
@Transactional
@@ -486,8 +488,6 @@
markLatePaymentBuilderService.getPaymentBuilder(dataContextOfAction, BigDecimal.ZERO, DateConverter.fromIsoString(command.getForTime()).toLocalDate(),
runningBalances);
- final LocalDateTime today = today();
-
final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
designatorToAccountIdentifierMapper,
"Marked late on " + command.getForTime(),
@@ -503,7 +503,58 @@
Action.MARK_LATE,
transactionUniqueifier);
- return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, DateConverter.toIsoString(today));
+ markCaseLate(dataContextOfAction, command.getForTime());
+
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getForTime());
+ }
+
+ @Transactional
+ @CommandHandler(logStart = CommandLogLevel.INFO, logFinish = CommandLogLevel.INFO)
+ @EventEmitter(
+ selectorName = IndividualLoanEventConstants.SELECTOR_NAME,
+ selectorValue = IndividualLoanEventConstants.MARK_IN_ARREARS_INDIVIDUALLOAN_CASE)
+ public IndividualLoanCommandEvent process(final MarkInArrearsCommand command) {
+ final String productIdentifier = command.getProductIdentifier();
+ final String caseIdentifier = command.getCaseIdentifier();
+ final DataContextOfAction dataContextOfAction = dataContextService.checkedGetDataContext(
+ productIdentifier, caseIdentifier, Collections.emptyList());
+ IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCaseEntity().getCurrentState()), Action.MARK_LATE);
+
+ checkIfTasksAreOutstanding(dataContextOfAction, Action.MARK_IN_ARREARS);
+
+ if (dataContextOfAction.getCustomerCaseEntity().getEndOfTerm() == null)
+ throw ServiceException.internalError(
+ "End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
+
+ final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
+ = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
+ final RealRunningBalances runningBalances = new RealRunningBalances(
+ accountingAdapter,
+ dataContextOfAction);
+
+ final PaymentBuilder paymentBuilder =
+ markInArrearsPaymentBuilderService.getPaymentBuilder(
+ dataContextOfAction,
+ BigDecimal.valueOf(command.getDaysLate()),
+ DateConverter.fromIsoString(command.getForTime()).toLocalDate(),
+ runningBalances);
+
+ final Optional<String> transactionUniqueifier = accountingAdapter.bookCharges(paymentBuilder.getBalanceAdjustments(),
+ designatorToAccountIdentifierMapper,
+ "Marked in arrears on " + command.getForTime(),
+ command.getForTime(),
+ dataContextOfAction.getMessageForCharge(Action.MARK_IN_ARREARS),
+ Action.MARK_IN_ARREARS.getTransactionType());
+
+ final CaseEntity customerCase = dataContextOfAction.getCustomerCaseEntity();
+
+ recordCommand(
+ command.getForTime(),
+ customerCase.getId(),
+ Action.MARK_IN_ARREARS,
+ transactionUniqueifier);
+
+ return new IndividualLoanCommandEvent(productIdentifier, caseIdentifier, command.getForTime());
}
@Transactional
@@ -529,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(),
@@ -549,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
@@ -573,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(),
@@ -586,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
@@ -610,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(),
@@ -630,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) {
@@ -669,7 +714,20 @@
caseCommandRepository.save(caseCommandEntity);
}
- private static LocalDateTime today() {
- return LocalDate.now(Clock.systemUTC()).atStartOfDay();
+ private void markCaseLate(
+ final DataContextOfAction dataContextOfAction,
+ final String forTime) {
+ final Optional<LateCaseEntity> lateCaseEntity = lateCaseRepository.findByCaseId(dataContextOfAction.getCustomerCaseEntity().getId());
+ if (!lateCaseEntity.isPresent()) {
+ final LateCaseEntity markCaseLate = new LateCaseEntity();
+ markCaseLate.setCaseId(dataContextOfAction.getCustomerCaseEntity().getId());
+ markCaseLate.setLateSince(DateConverter.fromIsoString(forTime));
+ lateCaseRepository.save(markCaseLate);
+ }
+ }
+
+ private void markCaseNotLate(
+ final DataContextOfAction dataContextOfAction) {
+ lateCaseRepository.deleteByCaseId(dataContextOfAction.getCustomerCaseEntity().getId());
}
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/repository/LateCaseEntity.java b/service/src/main/java/io/mifos/individuallending/internal/repository/LateCaseEntity.java
new file mode 100644
index 0000000..8fed47a
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/repository/LateCaseEntity.java
@@ -0,0 +1,80 @@
+/*
+ * 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.repository;
+
+import io.mifos.core.mariadb.util.LocalDateTimeConverter;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+/**
+ * @author Myrle Krantz
+ */
+@Entity
+@Table(name = "bastet_il_late_cases")
+public class LateCaseEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @Column(name = "case_id")
+ private Long caseId;
+
+ /** The date after the most recent payment due date at the time at which lateness was determined.
+ */
+ @Column(name = "late_since")
+ @Convert(converter = LocalDateTimeConverter.class)
+ private LocalDateTime lateSince;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public Long getCaseId() {
+ return caseId;
+ }
+
+ public void setCaseId(Long caseId) {
+ this.caseId = caseId;
+ }
+
+ public LocalDateTime getLateSince() {
+ return lateSince;
+ }
+
+ public void setLateSince(LocalDateTime lateSince) {
+ this.lateSince = lateSince;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LateCaseEntity that = (LateCaseEntity) o;
+ return Objects.equals(caseId, that.caseId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(caseId);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/repository/LateCaseRepository.java b/service/src/main/java/io/mifos/individuallending/internal/repository/LateCaseRepository.java
new file mode 100644
index 0000000..028cec6
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/repository/LateCaseRepository.java
@@ -0,0 +1,30 @@
+/*
+ * 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.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+/**
+ * @author Myrle Krantz
+ */
+@Repository
+public interface LateCaseRepository extends JpaRepository<LateCaseEntity, Long> {
+ Optional<LateCaseEntity> findByCaseId(Long caseId);
+ void deleteByCaseId(Long caseId);
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/LossProvisionStepService.java b/service/src/main/java/io/mifos/individuallending/internal/service/LossProvisionStepService.java
index 258f209..80d4dc9 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/LossProvisionStepService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/LossProvisionStepService.java
@@ -23,8 +23,6 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
-import java.math.BigDecimal;
-import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -34,12 +32,6 @@
*/
@Service
public class LossProvisionStepService {
- private final static List<LossProvisionStep> DEFAULT_LOSS_PROVISION_STEPS = Arrays.asList(
- new LossProvisionStep(0, BigDecimal.ONE),
- new LossProvisionStep(1, BigDecimal.valueOf(9)),
- new LossProvisionStep(30, BigDecimal.valueOf(30)),
- new LossProvisionStep(60, BigDecimal.valueOf(60)));
-
private final ProductRepository productRepository;
private final LossProvisionStepRepository lossProvisionStepRepository;
@@ -62,12 +54,8 @@
final Long productId = productRepository.findByIdentifier(productIdentifier)
.orElseThrow(() -> ServiceException.notFound("Product ''{}'' doesn''t exist.", productIdentifier))
.getId();
- final List<LossProvisionStep> ret = lossProvisionStepRepository.findByProductIdOrderByDaysLateAsc(productId)
+ return lossProvisionStepRepository.findByProductIdOrderByDaysLateAsc(productId)
.map(LossProvisionStepMapper::map)
.collect(Collectors.toList());
- if (!ret.isEmpty())
- return ret;
- else
- return DEFAULT_LOSS_PROVISION_STEPS;
}
}
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/MarkInArrearsPaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/MarkInArrearsPaymentBuilderService.java
new file mode 100644
index 0000000..fe66e9e
--- /dev/null
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/MarkInArrearsPaymentBuilderService.java
@@ -0,0 +1,71 @@
+/*
+ * 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.internal.repository.CaseParametersEntity;
+import io.mifos.individuallending.internal.service.DataContextOfAction;
+import io.mifos.individuallending.internal.service.schedule.LossProvisionChargesService;
+import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+@Service
+public class MarkInArrearsPaymentBuilderService implements PaymentBuilderService {
+ private final LossProvisionChargesService lossProvisionChargesService;
+
+ public MarkInArrearsPaymentBuilderService(
+ final LossProvisionChargesService lossProvisionChargesService) {
+ this.lossProvisionChargesService = lossProvisionChargesService;
+ }
+
+ @Override
+ public PaymentBuilder getPaymentBuilder(
+ final @Nonnull DataContextOfAction dataContextOfAction,
+ final @Nullable BigDecimal bigDecimalDaysLate,
+ final LocalDate forDate,
+ final RunningBalances runningBalances)
+ {
+ final CaseParametersEntity caseParameters = dataContextOfAction.getCaseParametersEntity();
+ final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
+
+ final BigDecimal loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize();
+
+ int daysLate = bigDecimalDaysLate == null ? 0 : bigDecimalDaysLate.intValueExact();
+
+ final List<ScheduledCharge> scheduledCharges = lossProvisionChargesService.getScheduledChargeForMarkInArrears(
+ dataContextOfAction, forDate, daysLate).map(Collections::singletonList).orElse(Collections.emptyList());
+
+ return CostComponentService.getCostComponentsForScheduledCharges(
+ scheduledCharges,
+ caseParameters.getBalanceRangeMaximum(),
+ runningBalances,
+ loanPaymentSize,
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ dataContextOfAction.getInterest(),
+ minorCurrencyUnitDigits,
+ true);
+ }
+}
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/MarkLatePaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/MarkLatePaymentBuilderService.java
index c02e10e..5406d2c 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/MarkLatePaymentBuilderService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/MarkLatePaymentBuilderService.java
@@ -18,6 +18,7 @@
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.LossProvisionChargesService;
import io.mifos.individuallending.internal.service.schedule.ScheduledAction;
import io.mifos.individuallending.internal.service.schedule.ScheduledCharge;
import io.mifos.individuallending.internal.service.schedule.ScheduledChargesService;
@@ -30,6 +31,7 @@
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
/**
* @author Myrle Krantz
@@ -37,10 +39,14 @@
@Service
public class MarkLatePaymentBuilderService implements PaymentBuilderService {
private final ScheduledChargesService scheduledChargesService;
+ private final LossProvisionChargesService lossProvisionChargesService;
@Autowired
- public MarkLatePaymentBuilderService(final ScheduledChargesService scheduledChargesService) {
+ public MarkLatePaymentBuilderService(
+ final ScheduledChargesService scheduledChargesService,
+ final LossProvisionChargesService lossProvisionChargesService) {
this.scheduledChargesService = scheduledChargesService;
+ this.lossProvisionChargesService = lossProvisionChargesService;
}
@Override
@@ -57,12 +63,15 @@
final BigDecimal loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize();
- final List<ScheduledCharge> scheduledChargesForThisAction = scheduledChargesService.getScheduledCharges(
+ final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(
productIdentifier,
Collections.singletonList(scheduledAction));
+ final Optional<ScheduledCharge> initialLossProvisionCharge = lossProvisionChargesService.getScheduledChargeForMarkLate(
+ dataContextOfAction, forDate);
+ initialLossProvisionCharge.ifPresent(scheduledCharges::add);
return CostComponentService.getCostComponentsForScheduledCharges(
- scheduledChargesForThisAction,
+ scheduledCharges,
caseParameters.getBalanceRangeMaximum(),
runningBalances,
loanPaymentSize,
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilder.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilder.java
index 4c2d646..da18f03 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilder.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilder.java
@@ -19,6 +19,7 @@
import io.mifos.core.lang.DateConverter;
import io.mifos.individuallending.IndividualLendingPatternFactory;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
+import io.mifos.individuallending.api.v1.domain.product.AccountDesignators;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import io.mifos.portfolio.api.v1.domain.CostComponent;
@@ -154,18 +155,20 @@
final BigDecimal plannedCharge) {
final BigDecimal expectedImpactOnDebitAccount = plannedCharge.subtract(this.getBalanceAdjustment(fromAccountDesignator));
final BigDecimal maxImpactOnDebitAccount = prePaymentBalances.getMaxDebit(fromAccountDesignator, expectedImpactOnDebitAccount);
- final BigDecimal maxDebit = maxImpactOnDebitAccount.add(this.getBalanceAdjustment(fromAccountDesignator))
- .max(BigDecimal.ZERO);
+ final BigDecimal maxDebit = (!fromAccountDesignator.equals(AccountDesignators.PRODUCT_LOSS_ALLOWANCE)) ?
+ maxImpactOnDebitAccount.add(this.getBalanceAdjustment(fromAccountDesignator)).max(BigDecimal.ZERO) :
+ maxImpactOnDebitAccount.add(this.getBalanceAdjustment(fromAccountDesignator));
final BigDecimal expectedImpactOnCreditAccount = plannedCharge.add(this.getBalanceAdjustment(toAccountDesignator));
final BigDecimal maxImpactOnCreditAccount = prePaymentBalances.getMaxCredit(toAccountDesignator, expectedImpactOnCreditAccount);
- final BigDecimal maxCredit = maxImpactOnCreditAccount.subtract(this.getBalanceAdjustment(toAccountDesignator))
- .max(BigDecimal.ZERO);
+ final BigDecimal maxCredit = (!toAccountDesignator.equals(AccountDesignators.GENERAL_LOSS_ALLOWANCE)) ?
+ maxImpactOnCreditAccount.subtract(this.getBalanceAdjustment(toAccountDesignator)).max(BigDecimal.ZERO) :
+ maxImpactOnCreditAccount.subtract(this.getBalanceAdjustment(toAccountDesignator));
return maxCredit.min(maxDebit);
}
private static boolean chargeIsAccrued(final ChargeDefinition chargeDefinition) {
- return chargeDefinition.getAccrualAccountDesignator() != null;
+ return chargeDefinition.getAccrualAccountDesignator() != null && chargeDefinition.getAccrueAction() != null;
}
private void addToBalance(
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderService.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderService.java
index 913982c..17350f5 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderService.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/PaymentBuilderService.java
@@ -21,6 +21,9 @@
import java.math.BigDecimal;
import java.time.LocalDate;
+/**
+ * @author Myrle Krantz
+ */
public interface PaymentBuilderService {
PaymentBuilder getPaymentBuilder(
diff --git a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java
index 40b5095..2821285 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/service/costcomponent/RealRunningBalances.java
@@ -90,14 +90,26 @@
@Override
public Optional<LocalDateTime> getStartOfTerm(final DataContextOfAction dataContextOfAction) {
- if (!startOfTerm.isPresent()) {
- final String customerLoanPrincipalAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
+ if (!startOfTerm.isPresent()) {
+ final String customerLoanPrincipalAccountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
- this.startOfTerm = accountingAdapter.getDateOfOldestEntryContainingMessage(
- customerLoanPrincipalAccountIdentifier,
- dataContextOfAction.getMessageForCharge(Action.DISBURSE));
- }
+ this.startOfTerm = accountingAdapter.getDateOfOldestEntryContainingMessage(
+ customerLoanPrincipalAccountIdentifier,
+ dataContextOfAction.getMessageForCharge(Action.DISBURSE));
+ }
return this.startOfTerm;
}
+
+ public BigDecimal getSumOfChargesForActionSinceDate(
+ final String accountDesignator,
+ final Action action,
+ final LocalDateTime since) {
+ final String accountIdentifier = designatorToAccountIdentifierMapper.mapOrThrow(accountDesignator);
+ return accountingAdapter.sumMatchingEntriesSinceDate(
+ accountIdentifier,
+ since.toLocalDate(),
+ dataContextOfAction.getMessageForCharge(action));
+
+ }
}
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 31f325f..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.
}};
@@ -87,7 +87,8 @@
}
default BigDecimal getMaxDebit(final String accountDesignator, final BigDecimal amount) {
- if (accountDesignator.equals(AccountDesignators.ENTRY))
+ if (accountDesignator.equals(AccountDesignators.ENTRY) ||
+ accountDesignator.equals(AccountDesignators.PRODUCT_LOSS_ALLOWANCE))
return amount;
if (ACCOUNT_SIGNS.get(accountDesignator).signum() == -1)
@@ -97,8 +98,12 @@
}
default BigDecimal getMaxCredit(final String accountDesignator, final BigDecimal amount) {
- if (accountDesignator.equals(AccountDesignators.ENTRY))
- return amount; //don't guard the entry account.
+ 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
+ // product loss allowance can achieve an "absolute" negative balance.
if (ACCOUNT_SIGNS.get(accountDesignator).signum() != -1)
return amount;
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..a9a6588 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,12 +15,15 @@
*/
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.ChargeDefinitionService;
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 io.mifos.portfolio.api.v1.domain.ChargeDefinition;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -28,19 +31,23 @@
import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.time.LocalDate;
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static io.mifos.individuallending.api.v1.domain.product.ChargeIdentifiers.*;
/**
* @author Myrle Krantz
*/
@Service
public class WriteOffPaymentBuilderService implements PaymentBuilderService {
- private final ScheduledChargesService scheduledChargesService;
+ final private ChargeDefinitionService chargeDefinitionService;
@Autowired
- public WriteOffPaymentBuilderService(final ScheduledChargesService scheduledChargesService) {
- this.scheduledChargesService = scheduledChargesService;
+ public WriteOffPaymentBuilderService(
+ final ChargeDefinitionService chargeDefinitionService) {
+ this.chargeDefinitionService = chargeDefinitionService;
}
@Override
@@ -51,16 +58,21 @@
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 Stream<ScheduledCharge> scheduledChargesForAccruals
+ = chargeDefinitionService.getChargeDefinitionsMappedByAccrueAction(dataContextOfAction.getProductEntity().getIdentifier())
+ .values().stream().flatMap(Collection::stream)
+ .map(x -> getReverseAccrualScheduledCharge(x, forDate));
+
+ final List<ScheduledCharge> scheduledChargesForAccrualsAndWriteOff = Stream.concat(scheduledChargesForAccruals,
+ Stream.of(getScheduledChargeForWriteOff(forDate)))
+ .collect(Collectors.toList());
final BigDecimal loanPaymentSize = dataContextOfAction.getCaseParametersEntity().getPaymentSize();
return CostComponentService.getCostComponentsForScheduledCharges(
- scheduledCharges,
+ scheduledChargesForAccrualsAndWriteOff,
caseParameters.getBalanceRangeMaximum(),
runningBalances,
loanPaymentSize,
@@ -70,4 +82,43 @@
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());
+ }
+
+ private ScheduledCharge getReverseAccrualScheduledCharge(
+ final ChargeDefinition accrualChargeDefinition,
+ final LocalDate forDate) {
+
+ final ChargeDefinition chargeDefinition = new ChargeDefinition();
+ chargeDefinition.setChargeAction(Action.WRITE_OFF.name());
+ chargeDefinition.setIdentifier(accrualChargeDefinition.getIdentifier());
+ chargeDefinition.setName(accrualChargeDefinition.getName());
+ chargeDefinition.setDescription(accrualChargeDefinition.getDescription());
+ chargeDefinition.setFromAccountDesignator(accrualChargeDefinition.getFromAccountDesignator());
+ chargeDefinition.setAccrualAccountDesignator(accrualChargeDefinition.getAccrualAccountDesignator());
+ chargeDefinition.setAccrueAction(accrualChargeDefinition.getAccrueAction());
+ chargeDefinition.setToAccountDesignator(AccountDesignators.PRODUCT_LOSS_ALLOWANCE);
+ chargeDefinition.setChargeMethod(accrualChargeDefinition.getChargeMethod());
+ chargeDefinition.setAmount(accrualChargeDefinition.getAmount());
+ 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 7bb2eb7..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
@@ -45,12 +44,19 @@
this.lossProvisionStepService = lossProvisionStepService;
}
- public Optional<ScheduledCharge> getScheduledChargeForMarkLate(
+ public Optional<ScheduledCharge> getScheduledChargeForMarkInArrears(
final DataContextOfAction dataContextOfAction,
final LocalDate forDate,
final int daysLate)
{
- return getScheduledLossProvisioningCharge(dataContextOfAction, forDate, daysLate, Action.MARK_LATE);
+ return getScheduledLossProvisioningCharge(dataContextOfAction, forDate, daysLate, Action.MARK_IN_ARREARS);
+ }
+
+ public Optional<ScheduledCharge> getScheduledChargeForMarkLate(
+ final DataContextOfAction dataContextOfAction,
+ final LocalDate forDate)
+ {
+ return getScheduledLossProvisioningCharge(dataContextOfAction, forDate, 1, Action.MARK_LATE);
}
@@ -58,14 +64,17 @@
final DataContextOfAction dataContextOfAction,
final LocalDate forDate)
{
- return getScheduledLossProvisioningCharge(dataContextOfAction, forDate, 0, Action.DISBURSE);
+ final Optional<ScheduledCharge> ret = getScheduledLossProvisioningCharge(dataContextOfAction, forDate, 0, Action.DISBURSE);
+ ret.ifPresent(x -> x.getChargeDefinition().setProportionalTo(ChargeProportionalDesignator.REQUESTED_REPAYMENT_DESIGNATOR.getValue()));
+ return ret;
}
private Optional<ScheduledCharge> getScheduledLossProvisioningCharge(
final DataContextOfAction dataContextOfAction,
final LocalDate forDate,
final int daysLate, Action action) {
- final Optional<ChargeDefinition> optionalChargeDefinition = percentProvision(dataContextOfAction, daysLate)
+ final Optional<ChargeDefinition> optionalChargeDefinition = lossProvisionStepService.findByProductIdAndDaysLate(dataContextOfAction.getProductEntity().getId(), daysLate)
+ .map(LossProvisionStep::getPercentProvision)
.map(percentProvision -> getLossProvisionCharge(percentProvision, action));
return optionalChargeDefinition.map(chargeDefinition -> {
@@ -74,14 +83,6 @@
});
}
- private Optional<BigDecimal> percentProvision(
- final DataContextOfAction dataContextOfAction,
- final int daysLate)
- {
- return lossProvisionStepService.findByProductIdAndDaysLate(dataContextOfAction.getProductEntity().getId(), daysLate)
- .map(LossProvisionStep::getPercentProvision);
- }
-
private ChargeDefinition getLossProvisionCharge(
final BigDecimal percentProvision,
final Action action) {
@@ -91,11 +92,10 @@
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.setToAccountDesignator(AccountDesignators.GENERAL_EXPENSE);
+ ret.setToAccountDesignator(AccountDesignators.GENERAL_LOSS_ALLOWANCE);
ret.setProportionalTo(ChargeProportionalDesignator.PRINCIPAL_DESIGNATOR.getValue());
ret.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
- ret.setAmount(percentProvision);
+ ret.setAmount(percentProvision.negate());
ret.setReadOnly(true);
return ret;
}
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/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
index 4bb8623..a3a0bc2 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/util/AccountingAdapter.java
@@ -180,20 +180,6 @@
.map(DateConverter::fromIsoString);
}
- public Optional<LocalDateTime> getDateOfMostRecentEntryContainingMessage(
- final String accountIdentifier,
- final String message) {
-
- final Account account = ledgerManager.findAccount(accountIdentifier);
- final LocalDateTime accountCreatedOn = DateConverter.fromIsoString(account.getCreatedOn());
- final DateRange fromAccountCreationUntilNow = oneSidedDateRange(accountCreatedOn.toLocalDate());
-
- return ledgerManager.fetchAccountEntriesStream(accountIdentifier, fromAccountCreationUntilNow.toString(), message, "DESC")
- .findFirst()
- .map(AccountEntry::getTransactionDate)
- .map(DateConverter::fromIsoString);
- }
-
public BigDecimal sumMatchingEntriesSinceDate(final String accountIdentifier, final LocalDate startDate, final String message)
{
final DateRange fromLastPaymentUntilNow = oneSidedDateRange(startDate);
@@ -245,7 +231,7 @@
generatedLedger.setName(ledgerIdentifer.getIdentifier());
}
}
- final boolean ledgerCreationDetected = expectation.waitForOccurrence(5, TimeUnit.SECONDS);
+ final boolean ledgerCreationDetected = expectation.waitForOccurrence(10, TimeUnit.SECONDS);
if (!ledgerCreationDetected)
logger.warn("Waited 5 seconds for creation of ledger '{}', but it was not detected. This could cause subsequent " +
"account creations to fail. Is there something wrong with the accounting service? Is ActiveMQ setup properly?",
diff --git a/service/src/main/resources/db/migrations/mariadb/V10__arrears_determination2.sql b/service/src/main/resources/db/migrations/mariadb/V10__arrears_determination2.sql
new file mode 100644
index 0000000..af50113
--- /dev/null
+++ b/service/src/main/resources/db/migrations/mariadb/V10__arrears_determination2.sql
@@ -0,0 +1,25 @@
+--
+-- 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.
+--
+
+CREATE TABLE bastet_il_late_cases (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ case_id BIGINT NOT NULL,
+ late_since TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT bastet_il_late_cases_pk PRIMARY KEY (id),
+ CONSTRAINT bastet_il_late_cases_uq UNIQUE (case_id),
+ CONSTRAINT bastet_il_late_cases_fk FOREIGN KEY (case_id) REFERENCES bastet_cases (id)
+);
\ No newline at end of file
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java
index 905f1f8..8fbcda1 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/AcceptPaymentBuilderServiceTest.java
@@ -31,6 +31,9 @@
import java.util.Map;
import java.util.stream.Collectors;
+/**
+ * @author Myrle Krantz
+ */
@RunWith(Parameterized.class)
public class AcceptPaymentBuilderServiceTest {
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java
index 7771678..80ba2a7 100644
--- a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/ApplyInterestPaymentBuilderServiceTest.java
@@ -24,6 +24,9 @@
import java.math.BigDecimal;
import java.util.Collections;
+/**
+ * @author Myrle Krantz
+ */
public class ApplyInterestPaymentBuilderServiceTest {
@Test
public void getPaymentBuilder() throws Exception {
diff --git a/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderServiceTest.java b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderServiceTest.java
new file mode 100644
index 0000000..4d3d907
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/DisbursePaymentBuilderServiceTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.product.LossProvisionStep;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.internal.service.LossProvisionStepService;
+import io.mifos.individuallending.internal.service.schedule.LossProvisionChargesService;
+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 org.mockito.Matchers;
+import org.mockito.Mockito;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author Myrle Krantz
+ */
+@RunWith(Parameterized.class)
+public class DisbursePaymentBuilderServiceTest {
+
+ @Parameterized.Parameters
+ public static Collection testCases() {
+ final Collection<PaymentBuilderServiceTestCase> ret = new ArrayList<>();
+ ret.add(simpleCase());
+ return ret;
+ }
+
+ private static PaymentBuilderServiceTestCase simpleCase() {
+ return new PaymentBuilderServiceTestCase("simple case");
+ }
+
+ private final PaymentBuilderServiceTestCase testCase;
+
+ public DisbursePaymentBuilderServiceTest(final PaymentBuilderServiceTestCase testCase) {
+ this.testCase = testCase;
+ }
+
+ @Test
+ public void getPaymentBuilder() throws Exception {
+ final LossProvisionStepService lossProvisionStepsService = Mockito.mock(LossProvisionStepService.class);
+ Mockito.doReturn(Optional.of(new LossProvisionStep(0, BigDecimal.ONE))).when(lossProvisionStepsService).findByProductIdAndDaysLate(Matchers.any(), Matchers.eq(0));
+ final LossProvisionChargesService lossProvisionChargesService = new LossProvisionChargesService(lossProvisionStepsService);
+ final PaymentBuilder paymentBuilder = PaymentBuilderServiceTestHarness.constructCallToPaymentBuilder(
+ (scheduledChargesService) -> new DisbursePaymentBuilderService(scheduledChargesService, lossProvisionChargesService), testCase);
+
+ final Payment payment = paymentBuilder.buildPayment(Action.DISBURSE, 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.paymentSize,
+ mappedCostComponents.get(ChargeIdentifiers.DISBURSE_PAYMENT_ID).getAmount());
+ Assert.assertEquals(
+ testCase.paymentSize.multiply(BigDecimal.valueOf(1, 2)).setScale(2, BigDecimal.ROUND_HALF_EVEN),
+ paymentBuilder.getBalanceAdjustments().get(AccountDesignators.PRODUCT_LOSS_ALLOWANCE));
+ Assert.assertEquals(
+ testCase.paymentSize.multiply(BigDecimal.valueOf(1, 2)).negate().setScale(2, BigDecimal.ROUND_HALF_EVEN),
+ paymentBuilder.getBalanceAdjustments().get(AccountDesignators.GENERAL_LOSS_ALLOWANCE));
+ }
+
+}
\ No newline at end of file
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..fe5f7d4
--- /dev/null
+++ b/service/src/test/java/io/mifos/individuallending/internal/service/costcomponent/WriteOffPaymentBuilderServiceTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.individuallending.internal.service.DefaultChargeDefinitionsMocker;
+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(DefaultChargeDefinitionsMocker.getChargeDefinitionService(Collections.emptyList())), 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());
+ }
+
+}