blob: 5ccbe3071fecf86e7853b5839d55cf1d0739218f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.fineract.cn.portfolio;
import static org.apache.fineract.cn.portfolio.Fixture.MINOR_CURRENCY_UNIT_DIGITS;
import com.google.gson.Gson;
import org.apache.fineract.cn.individuallending.api.v1.domain.caseinstance.CaseParameters;
import org.apache.fineract.cn.individuallending.api.v1.domain.caseinstance.PlannedPayment;
import org.apache.fineract.cn.individuallending.api.v1.domain.product.AccountDesignators;
import org.apache.fineract.cn.individuallending.api.v1.domain.product.ChargeIdentifiers;
import org.apache.fineract.cn.individuallending.api.v1.domain.product.ChargeProportionalDesignator;
import org.apache.fineract.cn.individuallending.api.v1.domain.product.LossProvisionConfiguration;
import org.apache.fineract.cn.individuallending.api.v1.domain.product.LossProvisionStep;
import org.apache.fineract.cn.individuallending.api.v1.domain.workflow.Action;
import org.apache.fineract.cn.individuallending.api.v1.events.IndividualLoanCommandEvent;
import org.apache.fineract.cn.individuallending.api.v1.events.IndividualLoanEventConstants;
import org.apache.fineract.cn.portfolio.api.v1.domain.AccountAssignment;
import org.apache.fineract.cn.portfolio.api.v1.domain.BalanceSegmentSet;
import org.apache.fineract.cn.portfolio.api.v1.domain.Case;
import org.apache.fineract.cn.portfolio.api.v1.domain.CaseStatus;
import org.apache.fineract.cn.portfolio.api.v1.domain.ChargeDefinition;
import org.apache.fineract.cn.portfolio.api.v1.domain.CostComponent;
import org.apache.fineract.cn.portfolio.api.v1.domain.ImportParameters;
import org.apache.fineract.cn.portfolio.api.v1.domain.Payment;
import org.apache.fineract.cn.portfolio.api.v1.domain.PaymentCycle;
import org.apache.fineract.cn.portfolio.api.v1.domain.Product;
import org.apache.fineract.cn.portfolio.api.v1.domain.TaskDefinition;
import org.apache.fineract.cn.portfolio.api.v1.events.BalanceSegmentSetEvent;
import org.apache.fineract.cn.portfolio.api.v1.events.ChargeDefinitionEvent;
import org.apache.fineract.cn.portfolio.api.v1.events.EventConstants;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.apache.fineract.cn.accounting.api.v1.domain.AccountType;
import org.apache.fineract.cn.accounting.api.v1.domain.Creditor;
import org.apache.fineract.cn.accounting.api.v1.domain.Debtor;
import org.apache.fineract.cn.api.util.ApiFactory;
import org.apache.fineract.cn.lang.DateConverter;
import org.apache.fineract.cn.rhythm.spi.v1.client.BeatListener;
import org.apache.fineract.cn.rhythm.spi.v1.domain.BeatPublish;
import org.apache.fineract.cn.rhythm.spi.v1.events.BeatPublishEvent;
import org.assertj.core.util.Sets;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* @author Myrle Krantz
*/
public class TestAccountingInteractionInLoanWorkflow extends AbstractPortfolioTest {
private static final BigDecimal PROCESSING_FEE_AMOUNT = BigDecimal.valueOf(50_00, MINOR_CURRENCY_UNIT_DIGITS);
private static final BigDecimal LOAN_ORIGINATION_FEE_AMOUNT = BigDecimal.valueOf(50_00, MINOR_CURRENCY_UNIT_DIGITS);
private static final BigDecimal DISBURSEMENT_FEE_LOWER_RANGE_AMOUNT = BigDecimal.valueOf(10_00, MINOR_CURRENCY_UNIT_DIGITS);
private static final BigDecimal DISBURSEMENT_FEE_UPPER_RANGE_AMOUNT = BigDecimal.valueOf(1_00, MINOR_CURRENCY_UNIT_DIGITS);
private static final BigDecimal IMPORTED_NEXT_REPAYMENT_AMOUNT = BigDecimal.valueOf(300_00, MINOR_CURRENCY_UNIT_DIGITS);
private static final String DISBURSEMENT_RANGES = "disbursement_ranges";
private static final String DISBURSEMENT_LOWER_RANGE = "smaller";
private static final String DISBURSEMENT_UPPER_RANGE = "larger";
private static final String UPPER_RANGE_DISBURSEMENT_FEE_ID = ChargeIdentifiers.DISBURSEMENT_FEE_ID + "2";
private BeatListener portfolioBeatListener;
private Product product = null;
private Case customerCase = null;
private TaskDefinition taskDefinition = null;
private String customerLoanPrincipalIdentifier = null;
private String customerLoanInterestIdentifier = null;
private String customerLoanFeeIdentifier = null;
private BigDecimal expectedCurrentPrincipal = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
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
public void prepBeatListener() {
portfolioBeatListener = new ApiFactory(logger).create(BeatListener.class, testEnvironment.serverURI());
}
@Test
public void workflowTerminatingInApplicationDenial() throws InterruptedException {
final LocalDateTime today = midnightToday();
step1CreateProduct();
step2CreateCase();
step3OpenCase(today);
step4DenyCase(today);
}
@Test
public void workflowImportingLoanFromExternalSystem() throws InterruptedException {
final LocalDateTime initialDisbursalDate = LocalDateTime.of(2017,11,8,0,0);
final LocalDateTime today = LocalDateTime.of(2017,11,22,0,0);
step1CreateProduct();
step2CreateCase();
step3IImportCaseWhenAccountsDontExistYet(initialDisbursalDate);
final List<PlannedPayment> plannedPayments = individualLending.getPaymentScheduleForCaseStream(
product.getIdentifier(),
customerCase.getIdentifier(),
null)
.collect(Collectors.toList());
int week = 3;
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 BigDecimal totalDue = expectedCurrentPrincipal.add(interestAccrued).add(nonLateFees);
if (totalDue.compareTo(IMPORTED_NEXT_REPAYMENT_AMOUNT) >= 0)
Assert.assertEquals(IMPORTED_NEXT_REPAYMENT_AMOUNT, nextRepaymentAmount);
else
Assert.assertEquals(totalDue, nextRepaymentAmount);
final Payment payment = step7PaybackPartialAmount(
nextRepaymentAmount,
today.plusDays((week + 1) * 7),
BigDecimal.ZERO,
nextRepaymentAmount,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
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);
Assert.assertEquals("week " + week, interestAccruedBeforePayment, customerLoanInterest);
//TODO: Assert.assertEquals("week " + week, plannedPayments.get(week+1).getPayment(), payment);
week++;
}
step8Close(DateConverter
.fromIsoString(plannedPayments.get(plannedPayments.size()-1).getPayment().getDate()));
}
@Test
public void workflowImportingLoanFromExternalSystemConnectingToExistingAccounts() throws InterruptedException {
final LocalDateTime initialDisbursalDate = LocalDateTime.of(2017,11,8,0,0);
final LocalDateTime today = LocalDateTime.of(2017,11,22,0,0);
step1CreateProduct();
step2CreateCase();
step3IImportCaseWhenAccountsExist(initialDisbursalDate);
final BigDecimal payment = expectedCurrentPrincipal.add(nonLateFees).add(interestAccrued);
step7PaybackPartialAmount(
payment,
today,
BigDecimal.ZERO,
payment,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
step8Close(today);
}
@Test
public void cantChangeDeniedCase() throws InterruptedException {
final LocalDateTime today = midnightToday();
step1CreateProduct();
step2CreateCase();
step3OpenCase(today);
step4DenyCase(today);
try {
customerCase.setInterest(BigDecimal.ONE);
portfolioManager.changeCase(product.getIdentifier(), customerCase.getIdentifier(), customerCase);
Assert.fail("Changing a denied case should fail.");
}
catch (IllegalArgumentException ignored) {
}
}
@Test
public void cantChangeApprovedCase() throws InterruptedException {
final LocalDateTime today = midnightToday();
step1CreateProduct();
step2CreateCase();
step3OpenCase(today);
step4ApproveCase(today);
try {
customerCase.setInterest(BigDecimal.ONE);
portfolioManager.changeCase(product.getIdentifier(), customerCase.getIdentifier(), customerCase);
Assert.fail("Changing a denied case should fail.");
}
catch (IllegalArgumentException ignored) {
}
}
@Test(expected = IllegalArgumentException.class)
public void cantRequestCostComponentsForInvalidAction() throws InterruptedException {
final LocalDateTime today = midnightToday();
step1CreateProduct();
step2CreateCase();
portfolioManager.getCostComponentsForAction(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE.name(),
Collections.emptySet(),
BigDecimal.TEN,
DateConverter.toIsoString(today)
);
}
//TODO: once we've upgraded to junit 5 replace workflowTerminatingInEarlyLoanPayoff,
// and workflowDisbursalAndPayoffFromTellerAccount with one test annotated with
//@ParameterizedTest
//@ValueSource(strings = { AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT, AccountingFixture.TELLER_ONE_ACCOUNT })
@Test
public void workflowTerminatingInEarlyLoanPayoff() 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),
BigDecimal.ZERO,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), BigDecimal.ZERO);
final BigDecimal payment = expectedCurrentPrincipal.add(nonLateFees).add(interestAccrued);
step7PaybackPartialAmount(
payment,
today,
BigDecimal.ZERO,
payment,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
step8Close(today);
}
@Test
public void workflowDisbursalAndPayoffFromTellerAccount() 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),
BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
AccountingFixture.TELLER_ONE_ACCOUNT);
step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), BigDecimal.ZERO);
final BigDecimal payment = expectedCurrentPrincipal.add(nonLateFees).add(interestAccrued);
step7PaybackPartialAmount(
payment,
today,
BigDecimal.ZERO,
BigDecimal.ZERO, //payback should work even if there's *nothing* in the teller account at initiation.
AccountingFixture.TELLER_ONE_ACCOUNT);
step8Close(today);
}
@Test
public void workflowWithTwoUnequalDisbursals() throws InterruptedException {
final LocalDateTime today = midnightToday();
step1CreateProduct();
step2CreateCase();
step3OpenCase(today);
step4ApproveCase(today);
step5Disburse(
BigDecimal.valueOf(500_00, MINOR_CURRENCY_UNIT_DIGITS),
today,
ChargeIdentifiers.DISBURSEMENT_FEE_ID,
BigDecimal.valueOf(10_00, MINOR_CURRENCY_UNIT_DIGITS),
BigDecimal.ZERO,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
step5Disburse(
BigDecimal.valueOf(1_500_00, MINOR_CURRENCY_UNIT_DIGITS),
today,
UPPER_RANGE_DISBURSEMENT_FEE_ID,
BigDecimal.valueOf(15_00, MINOR_CURRENCY_UNIT_DIGITS),
BigDecimal.ZERO,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), BigDecimal.ZERO);
final BigDecimal payment = expectedCurrentPrincipal.add(nonLateFees).add(interestAccrued);
step7PaybackPartialAmount(
payment,
today,
BigDecimal.ZERO,
payment,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
step8Close(today);
}
@Test
public void workflowWithTwoNearlyEqualRepayments() 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),
BigDecimal.ZERO,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
step6CalculateInterestAccrualAndCheckForLateness(midnightToday(), BigDecimal.ZERO);
final BigDecimal repayment1 = expectedCurrentPrincipal.divide(BigDecimal.valueOf(2), BigDecimal.ROUND_HALF_EVEN)
.setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
step7PaybackPartialAmount(
repayment1,
today,
BigDecimal.ZERO,
repayment1,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
step7PaybackPartialAmount(
expectedCurrentPrincipal,
today,
BigDecimal.ZERO,
expectedCurrentPrincipal,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
step8Close(today);
}
@Test
public void workflowWithNegativePaymentSize() throws InterruptedException {
final LocalDateTime today = midnightToday();
step1CreateProduct();
step2CreateCase();
step3OpenCase(today);
step4ApproveCase(today);
try {
step5Disburse(
BigDecimal.valueOf(-2).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN),
today,
UPPER_RANGE_DISBURSEMENT_FEE_ID,
BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS,BigDecimal.ROUND_HALF_EVEN),
BigDecimal.ZERO,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
Assert.fail("Expected an IllegalArgumentException.");
}
catch (IllegalArgumentException ignored) { }
}
@Test
public void workflowWithNormalRepayment() throws InterruptedException {
final LocalDateTime today = midnightToday();
step1CreateProduct();
step2CreateCase();
step3OpenCase(today);
step4ApproveCase(today);
final List<PlannedPayment> plannedPayments = individualLending.getPaymentScheduleForCaseStream(
product.getIdentifier(),
customerCase.getIdentifier(),
null)
.collect(Collectors.toList());
step5Disburse(
BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
today,
UPPER_RANGE_DISBURSEMENT_FEE_ID,
BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS),
BigDecimal.ZERO,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
int week = 0;
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,
nextRepaymentAmount,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
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);
Assert.assertEquals("week " + week, interestAccruedBeforePayment, customerLoanInterest);
Assert.assertEquals("week " + week, plannedPayments.get(week+1).getPayment(), payment);
week++;
}
Assert.assertEquals(week+1, plannedPayments.size());
step8Close(DateConverter.fromIsoString(plannedPayments.get(plannedPayments.size()-1).getPayment().getDate()));
}
@Test
public void workflowWithOneLateRepayment() throws InterruptedException {
final LocalDateTime today = midnightToday();
step1CreateProduct();
step2CreateCase();
step3OpenCase(today);
step4ApproveCase(today);
final List<PlannedPayment> plannedPayments = individualLending.getPaymentScheduleForCaseStream(
product.getIdentifier(),
customerCase.getIdentifier(),
null)
.collect(Collectors.toList());
step5Disburse(
BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS),
today,
UPPER_RANGE_DISBURSEMENT_FEE_ID,
BigDecimal.valueOf(20_00, MINOR_CURRENCY_UNIT_DIGITS),
BigDecimal.ZERO,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
int week = 0;
final int weekOfLateRepayment = 3;
while (expectedCurrentPrincipal.compareTo(BigDecimal.ZERO) > 0) {
logger.info("Simulating week {}. Expected current balance {}.", week, expectedCurrentPrincipal);
if (week == weekOfLateRepayment) {
final BigDecimal lateFee = BigDecimal.valueOf(15_36, MINOR_CURRENCY_UNIT_DIGITS); //??? TODO: check the late fee value.
step6CalculateInterestAndCheckForLatenessForRangeOfDays(
today,
(week * 7) + 1,
(week + 1) * 7 + 2,
7,
lateFee);
final BigDecimal nextRepaymentAmount = findNextRepaymentAmount(today.plusDays((week + 1) * 7 + 2));
step7PaybackPartialAmount(
nextRepaymentAmount,
today.plusDays((week + 1) * 7 + 2),
lateFee,
nextRepaymentAmount,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
}
else {
step6CalculateInterestAndCheckForLatenessForWeek(today, week);
final BigDecimal nextRepaymentAmount = findNextRepaymentAmount(today.plusDays((week + 1) * 7));
final Payment payment = step7PaybackPartialAmount(
nextRepaymentAmount,
today.plusDays((week + 1) * 7),
BigDecimal.ZERO,
nextRepaymentAmount,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
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(interestAccrual.negate(), customerLoanInterest);
//Assert.assertEquals(plannedPayments.get(week+1).getPayment(), payment);
}
week++;
}
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),
BigDecimal.ZERO,
AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT);
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) {
AccountingFixture.mockBalance(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT, BigDecimal.valueOf(2000_00L, 2));
final Payment nextPayment = portfolioManager.getCostComponentsForAction(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.ACCEPT_PAYMENT.name(),
null,
null,
DateConverter.toIsoString(forDateTime));
final BigDecimal nextRepaymentAmount = nextPayment.getBalanceAdjustments()
.getOrDefault(AccountDesignators.ENTRY, BigDecimal.ZERO).negate();
Assert.assertTrue(nextRepaymentAmount.signum() != -1);
return nextRepaymentAmount;
}
//Create product and set charges to fixed fees.
private void step1CreateProduct() throws InterruptedException {
logger.info("step1CreateProduct");
product = createProduct();
final BalanceSegmentSet balanceSegmentSet = new BalanceSegmentSet();
balanceSegmentSet.setIdentifier(DISBURSEMENT_RANGES);
balanceSegmentSet.setSegmentIdentifiers(Arrays.asList(DISBURSEMENT_LOWER_RANGE, DISBURSEMENT_UPPER_RANGE));
balanceSegmentSet.setSegments(Arrays.asList(BigDecimal.ZERO, BigDecimal.valueOf(1_000_0000, 4)));
portfolioManager.createBalanceSegmentSet(product.getIdentifier(), balanceSegmentSet);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_BALANCE_SEGMENT_SET, new BalanceSegmentSetEvent(product.getIdentifier(), balanceSegmentSet.getIdentifier())));
setFeeToFixedValue(product.getIdentifier(), ChargeIdentifiers.PROCESSING_FEE_ID, PROCESSING_FEE_AMOUNT);
setFeeToFixedValue(product.getIdentifier(), ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, LOAN_ORIGINATION_FEE_AMOUNT);
final ChargeDefinition lowerRangeDisbursementFeeChargeDefinition
= portfolioManager.getChargeDefinition(product.getIdentifier(), ChargeIdentifiers.DISBURSEMENT_FEE_ID);
lowerRangeDisbursementFeeChargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.FIXED);
lowerRangeDisbursementFeeChargeDefinition.setAmount(DISBURSEMENT_FEE_LOWER_RANGE_AMOUNT);
lowerRangeDisbursementFeeChargeDefinition.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
lowerRangeDisbursementFeeChargeDefinition.setForSegmentSet(DISBURSEMENT_RANGES);
lowerRangeDisbursementFeeChargeDefinition.setFromSegment(DISBURSEMENT_LOWER_RANGE);
lowerRangeDisbursementFeeChargeDefinition.setToSegment(DISBURSEMENT_LOWER_RANGE);
portfolioManager.changeChargeDefinition(product.getIdentifier(), ChargeIdentifiers.DISBURSEMENT_FEE_ID, lowerRangeDisbursementFeeChargeDefinition);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CHARGE_DEFINITION,
new ChargeDefinitionEvent(product.getIdentifier(), ChargeIdentifiers.DISBURSEMENT_FEE_ID)));
final ChargeDefinition upperRangeDisbursementFeeChargeDefinition = new ChargeDefinition();
upperRangeDisbursementFeeChargeDefinition.setIdentifier(UPPER_RANGE_DISBURSEMENT_FEE_ID);
upperRangeDisbursementFeeChargeDefinition.setName(UPPER_RANGE_DISBURSEMENT_FEE_ID);
upperRangeDisbursementFeeChargeDefinition.setDescription(lowerRangeDisbursementFeeChargeDefinition.getDescription());
upperRangeDisbursementFeeChargeDefinition.setFromAccountDesignator(lowerRangeDisbursementFeeChargeDefinition.getFromAccountDesignator());
upperRangeDisbursementFeeChargeDefinition.setToAccountDesignator(lowerRangeDisbursementFeeChargeDefinition.getToAccountDesignator());
upperRangeDisbursementFeeChargeDefinition.setAccrualAccountDesignator(lowerRangeDisbursementFeeChargeDefinition.getAccrualAccountDesignator());
upperRangeDisbursementFeeChargeDefinition.setAccrueAction(lowerRangeDisbursementFeeChargeDefinition.getAccrueAction());
upperRangeDisbursementFeeChargeDefinition.setChargeAction(lowerRangeDisbursementFeeChargeDefinition.getChargeAction());
upperRangeDisbursementFeeChargeDefinition.setChargeMethod(ChargeDefinition.ChargeMethod.PROPORTIONAL);
upperRangeDisbursementFeeChargeDefinition.setAmount(DISBURSEMENT_FEE_UPPER_RANGE_AMOUNT);
upperRangeDisbursementFeeChargeDefinition.setProportionalTo(ChargeProportionalDesignator.REQUESTED_DISBURSEMENT_DESIGNATOR.getValue());
upperRangeDisbursementFeeChargeDefinition.setForSegmentSet(DISBURSEMENT_RANGES);
upperRangeDisbursementFeeChargeDefinition.setFromSegment(DISBURSEMENT_UPPER_RANGE);
upperRangeDisbursementFeeChargeDefinition.setToSegment(DISBURSEMENT_UPPER_RANGE);
portfolioManager.createChargeDefinition(product.getIdentifier(), upperRangeDisbursementFeeChargeDefinition);
Assert.assertTrue(this.eventRecorder.wait(EventConstants.POST_CHARGE_DEFINITION,
new ChargeDefinitionEvent(product.getIdentifier(), UPPER_RANGE_DISBURSEMENT_FEE_ID)));
taskDefinition = createTaskDefinition(product);
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 {
logger.info("step2CreateCase");
final CaseParameters caseParameters = Fixture.createAdjustedCaseParameters(x ->
x.setPaymentCycle(new PaymentCycle(ChronoUnit.WEEKS, 1, null, null, null))
);
final String caseParametersAsString = new Gson().toJson(caseParameters);
customerCase = createAdjustedCase(product.getIdentifier(), x -> x.setParameters(caseParametersAsString));
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.OPEN, Action.IMPORT);
}
//Open the case and accept a processing fee.
private void step3OpenCase(final LocalDateTime forDateTime) throws InterruptedException {
logger.info("step3OpenCase");
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.OPEN,
null,
null,
forDateTime,
MINOR_CURRENCY_UNIT_DIGITS);
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.OPEN,
forDateTime,
assignEntry(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT),
IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE,
Case.State.PENDING);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.APPROVE, Action.DENY);
}
//Import an active case.
private void step3IImportCaseWhenAccountsDontExistYet(final LocalDateTime forDateTime) throws InterruptedException {
logger.info("step3IImportCaseWhenAccountsDontExistYet");
final BigDecimal currentPrincipal = BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS);
final AccountAssignment customerLoanPrincipalAccountAssignment = new AccountAssignment();
customerLoanPrincipalAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
customerLoanPrincipalAccountAssignment.setAlternativeAccountNumber("external-system-sourced-customer-loan-principal-account-identifier");
customerLoanPrincipalAccountAssignment.setLedgerIdentifier(AccountDesignators.CUSTOMER_LOAN_GROUP);
final AccountAssignment customerLoanInterestAccountAssignment = new AccountAssignment();
customerLoanInterestAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_INTEREST);
customerLoanInterestAccountAssignment.setAlternativeAccountNumber("external-system-sourced-customer-loan-interest-account-identifier");
customerLoanInterestAccountAssignment.setLedgerIdentifier(AccountDesignators.CUSTOMER_LOAN_GROUP);
final AccountAssignment customerLoanFeeAccountAssignment = new AccountAssignment();
customerLoanFeeAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_FEES);
customerLoanFeeAccountAssignment.setAlternativeAccountNumber("external-system-sourced-customer-loan-fees-account-identifier");
customerLoanFeeAccountAssignment.setLedgerIdentifier(AccountDesignators.CUSTOMER_LOAN_GROUP);
final ArrayList<AccountAssignment> importAccountAssignments = new ArrayList<>();
importAccountAssignments.add(customerLoanPrincipalAccountAssignment);
importAccountAssignments.add(customerLoanInterestAccountAssignment);
importAccountAssignments.add(customerLoanFeeAccountAssignment);
final ImportParameters importParameters = new ImportParameters();
importParameters.setCaseAccountAssignments(importAccountAssignments);
importParameters.setPaymentSize(IMPORTED_NEXT_REPAYMENT_AMOUNT);
importParameters.setCreatedOn(DateConverter.toIsoString(forDateTime));
importParameters.setCurrentBalances(Collections.singletonMap(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, currentPrincipal));
importParameters.setStartOfTerm(DateConverter.toIsoString(forDateTime));
portfolioManager.executeImportCommand(product.getIdentifier(), customerCase.getIdentifier(), importParameters);
Assert.assertTrue(eventRecorder.wait(IndividualLoanEventConstants.IMPORT_INDIVIDUALLOAN_CASE, new IndividualLoanCommandEvent(product.getIdentifier(), customerCase.getIdentifier(), DateConverter.toIsoString(forDateTime))));
final String customerLoanLedgerIdentifier = AccountingFixture.verifyLedgerCreation(
ledgerManager,
AccountingFixture.CUSTOMER_LOAN_LEDGER_IDENTIFIER,
AccountType.ASSET);
customerLoanPrincipalIdentifier =
AccountingFixture.verifyAccountCreationMatchingDesignator(
ledgerManager, customerLoanLedgerIdentifier,
AccountDesignators.CUSTOMER_LOAN_PRINCIPAL,
customerLoanPrincipalAccountAssignment.getAlternativeAccountNumber(),
AccountType.ASSET,
currentPrincipal);
customerLoanInterestIdentifier =
AccountingFixture.verifyAccountCreationMatchingDesignator(
ledgerManager,
customerLoanLedgerIdentifier,
AccountDesignators.CUSTOMER_LOAN_INTEREST,
customerLoanInterestAccountAssignment.getAlternativeAccountNumber(),
AccountType.ASSET,
BigDecimal.ZERO);
customerLoanFeeIdentifier =
AccountingFixture.verifyAccountCreationMatchingDesignator(
ledgerManager,
customerLoanLedgerIdentifier,
AccountDesignators.CUSTOMER_LOAN_FEES,
customerLoanFeeAccountAssignment.getAlternativeAccountNumber(),
AccountType.ASSET,
BigDecimal.ZERO);
final CaseStatus changedCaseStatus = portfolioManager.getCaseStatus(product.getIdentifier(), customerCase.getIdentifier());
Assert.assertEquals(Case.State.ACTIVE.name(), changedCaseStatus.getCurrentState());
Assert.assertEquals(DateConverter.toIsoString(forDateTime), changedCaseStatus.getStartOfTerm());
Assert.assertEquals("2018-02-08T00:00:00Z", changedCaseStatus.getEndOfTerm());
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.MARK_IN_ARREARS, Action.WRITE_OFF, Action.CLOSE);
expectedCurrentPrincipal = currentPrincipal;
updateBalanceMock();
final Case changedCase = portfolioManager.getCase(product.getIdentifier(), customerCase.getIdentifier());
final Map<String, String> designatorsAssignedForCase = changedCase.getAccountAssignments().stream()
.collect(Collectors.toMap(AccountAssignment::getDesignator, AccountAssignment::getAccountIdentifier));
Assert.assertEquals(
customerLoanPrincipalIdentifier,
designatorsAssignedForCase.get(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL));
Assert.assertEquals(
customerLoanInterestIdentifier,
designatorsAssignedForCase.get(AccountDesignators.CUSTOMER_LOAN_INTEREST));
Assert.assertEquals(
customerLoanFeeIdentifier,
designatorsAssignedForCase.get(AccountDesignators.CUSTOMER_LOAN_FEES));
}
//Import an active case.
private void step3IImportCaseWhenAccountsExist(final LocalDateTime forDateTime) throws InterruptedException {
logger.info("step3IImportCaseWhenAccountsExist");
final BigDecimal currentPrincipal = BigDecimal.valueOf(2_000_00, MINOR_CURRENCY_UNIT_DIGITS);
final AccountAssignment customerLoanPrincipalAccountAssignment = new AccountAssignment();
customerLoanPrincipalAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL);
customerLoanPrincipalAccountAssignment.setAccountIdentifier(AccountingFixture.IMPORTED_CUSTOMER_LOAN_PRINCIPAL_ACCOUNT);
customerLoanPrincipalAccountAssignment.setAlternativeAccountNumber("external-system-sourced-customer-loan-principal-account-identifier");
final AccountAssignment customerLoanInterestAccountAssignment = new AccountAssignment();
customerLoanInterestAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_INTEREST);
customerLoanInterestAccountAssignment.setAccountIdentifier(AccountingFixture.IMPORTED_CUSTOMER_LOAN_INTEREST_ACCOUNT);
customerLoanInterestAccountAssignment.setAlternativeAccountNumber("external-system-sourced-customer-loan-interest-account-identifier");
final AccountAssignment customerLoanFeeAccountAssignment = new AccountAssignment();
customerLoanFeeAccountAssignment.setDesignator(AccountDesignators.CUSTOMER_LOAN_FEES);
customerLoanFeeAccountAssignment.setAccountIdentifier(AccountingFixture.IMPORTED_CUSTOMER_LOAN_FEES_ACCOUNT);
customerLoanFeeAccountAssignment.setAlternativeAccountNumber("external-system-sourced-customer-loan-fees-account-identifier");
final ArrayList<AccountAssignment> importAccountAssignments = new ArrayList<>();
importAccountAssignments.add(customerLoanPrincipalAccountAssignment);
importAccountAssignments.add(customerLoanInterestAccountAssignment);
importAccountAssignments.add(customerLoanFeeAccountAssignment);
final ImportParameters importParameters = new ImportParameters();
importParameters.setCaseAccountAssignments(importAccountAssignments);
importParameters.setPaymentSize(IMPORTED_NEXT_REPAYMENT_AMOUNT);
importParameters.setCreatedOn(DateConverter.toIsoString(forDateTime));
importParameters.setCurrentBalances(Collections.singletonMap(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, currentPrincipal));
importParameters.setStartOfTerm(DateConverter.toIsoString(forDateTime));
portfolioManager.executeImportCommand(product.getIdentifier(), customerCase.getIdentifier(), importParameters);
Assert.assertTrue(eventRecorder.wait(IndividualLoanEventConstants.IMPORT_INDIVIDUALLOAN_CASE, new IndividualLoanCommandEvent(product.getIdentifier(), customerCase.getIdentifier(), DateConverter.toIsoString(forDateTime))));
AccountingFixture.verifyLedgerCreation(
ledgerManager,
AccountingFixture.CUSTOMER_LOAN_LEDGER_IDENTIFIER,
AccountType.ASSET);
customerLoanPrincipalIdentifier = AccountingFixture.IMPORTED_CUSTOMER_LOAN_PRINCIPAL_ACCOUNT;
customerLoanInterestIdentifier = AccountingFixture.IMPORTED_CUSTOMER_LOAN_INTEREST_ACCOUNT;
customerLoanFeeIdentifier = AccountingFixture.IMPORTED_CUSTOMER_LOAN_FEES_ACCOUNT;
final CaseStatus changedCaseStatus = portfolioManager.getCaseStatus(product.getIdentifier(), customerCase.getIdentifier());
Assert.assertEquals(Case.State.ACTIVE.name(), changedCaseStatus.getCurrentState());
Assert.assertEquals(DateConverter.toIsoString(forDateTime), changedCaseStatus.getStartOfTerm());
Assert.assertEquals("2018-02-08T00:00:00Z", changedCaseStatus.getEndOfTerm());
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
Action.APPLY_INTEREST, Action.MARK_LATE, Action.ACCEPT_PAYMENT, Action.DISBURSE, Action.MARK_IN_ARREARS, Action.WRITE_OFF, Action.CLOSE);
expectedCurrentPrincipal = currentPrincipal;
updateBalanceMock();
final Case changedCase = portfolioManager.getCase(product.getIdentifier(), customerCase.getIdentifier());
final Map<String, String> designatorsAssignedForCase = changedCase.getAccountAssignments().stream()
.collect(Collectors.toMap(AccountAssignment::getDesignator, AccountAssignment::getAccountIdentifier));
Assert.assertEquals(
AccountingFixture.IMPORTED_CUSTOMER_LOAN_PRINCIPAL_ACCOUNT,
designatorsAssignedForCase.get(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL));
Assert.assertEquals(
AccountingFixture.IMPORTED_CUSTOMER_LOAN_INTEREST_ACCOUNT,
designatorsAssignedForCase.get(AccountDesignators.CUSTOMER_LOAN_INTEREST));
Assert.assertEquals(
AccountingFixture.IMPORTED_CUSTOMER_LOAN_FEES_ACCOUNT,
designatorsAssignedForCase.get(AccountDesignators.CUSTOMER_LOAN_FEES));
}
//Deny the case. Once this is done, no more actions are possible for the case.
private void step4DenyCase(final LocalDateTime forDateTime) throws InterruptedException {
logger.info("step4DenyCase");
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DENY,
null,
null,
forDateTime,
MINOR_CURRENCY_UNIT_DIGITS);
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DENY,
forDateTime,
assignEntry(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT),
IndividualLoanEventConstants.DENY_INDIVIDUALLOAN_CASE,
Case.State.CLOSED);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier());
}
//Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
private void step4ApproveCase(final LocalDateTime forDateTime) throws InterruptedException
{
logger.info("step4ApproveCase");
markTaskExecuted(product, customerCase, taskDefinition);
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.APPROVE,
null,
null,
forDateTime,
MINOR_CURRENCY_UNIT_DIGITS);
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.APPROVE,
forDateTime,
assignEntry(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT),
IndividualLoanEventConstants.APPROVE_INDIVIDUALLOAN_CASE,
Case.State.APPROVED);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(), Action.DISBURSE, Action.CLOSE);
final String customerLoanLedgerIdentifier = AccountingFixture.verifyLedgerCreation(
ledgerManager,
AccountingFixture.CUSTOMER_LOAN_LEDGER_IDENTIFIER,
AccountType.ASSET);
customerLoanPrincipalIdentifier =
AccountingFixture.verifyAccountCreationMatchingDesignator(ledgerManager, customerLoanLedgerIdentifier, AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, AccountType.ASSET);
customerLoanInterestIdentifier =
AccountingFixture.verifyAccountCreationMatchingDesignator(ledgerManager, customerLoanLedgerIdentifier, AccountDesignators.CUSTOMER_LOAN_INTEREST, AccountType.ASSET);
customerLoanFeeIdentifier =
AccountingFixture.verifyAccountCreationMatchingDesignator(ledgerManager, customerLoanLedgerIdentifier, AccountDesignators.CUSTOMER_LOAN_FEES, AccountType.ASSET);
expectedCurrentPrincipal = BigDecimal.ZERO;
interestAccrued = BigDecimal.ZERO;
nonLateFees = BigDecimal.ZERO;
lateFees = BigDecimal.ZERO;
updateBalanceMock();
}
//Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
private void step5Disburse(
final BigDecimal amount,
final LocalDateTime forDateTime,
final String whichDisbursementFee,
final BigDecimal disbursementFeeAmount,
final BigDecimal balanceInEntryAccount,
final String entryAccountIdentifier) throws InterruptedException {
logger.info("step5Disburse '{}'", amount);
final BigDecimal provisionForLosses = amount.multiply(BigDecimal.valueOf(0.01)).setScale(MINOR_CURRENCY_UNIT_DIGITS, BigDecimal.ROUND_HALF_EVEN);
AccountingFixture.mockBalance(entryAccountIdentifier, balanceInEntryAccount);
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE,
Sets.newLinkedHashSet(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN_GROUP),
amount,
forDateTime,
MINOR_CURRENCY_UNIT_DIGITS,
new CostComponent(whichDisbursementFee, disbursementFeeAmount),
new CostComponent(ChargeIdentifiers.LOAN_ORIGINATION_FEE_ID, LOAN_ORIGINATION_FEE_AMOUNT),
new CostComponent(ChargeIdentifiers.PROCESSING_FEE_ID, PROCESSING_FEE_AMOUNT),
new CostComponent(ChargeIdentifiers.DISBURSE_PAYMENT_ID, amount));
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.DISBURSE,
forDateTime,
assignEntry(entryAccountIdentifier),
amount,
IndividualLoanEventConstants.DISBURSE_INDIVIDUALLOAN_CASE,
Case.State.ACTIVE);
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
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(entryAccountIdentifier, 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();
}
private void step6CalculateInterestAndCheckForLatenessForWeek(
final LocalDateTime referenceDate,
final int weekNumber) throws InterruptedException {
step6CalculateInterestAndCheckForLatenessForRangeOfDays(
referenceDate,
(weekNumber * 7) + 1,
(weekNumber + 1) * 7,
-1,
BigDecimal.ZERO);
}
private void step6CalculateInterestAndCheckForLatenessForRangeOfDays(
final LocalDateTime referenceDate,
final int startInclusive,
final int endInclusive,
final int relativeDayOfLateFee,
final BigDecimal calculatedLateFee) throws InterruptedException {
try {
final LocalDateTime absoluteDayOfLateFee = referenceDate.plusDays(startInclusive + relativeDayOfLateFee);
IntStream.rangeClosed(startInclusive, endInclusive)
.mapToObj(referenceDate::plusDays)
.forEach(day -> {
try {
if (day.equals(absoluteDayOfLateFee)) {
step6CalculateInterestAccrualAndCheckForLateness(day, calculatedLateFee);
}
else {
step6CalculateInterestAccrualAndCheckForLateness(day, BigDecimal.ZERO);
}
} 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;
}
}
//Perform daily interest calculation.
private void step6CalculateInterestAccrualAndCheckForLateness(
final LocalDateTime forDateTime,
final BigDecimal calculatedLateFee) throws InterruptedException {
logger.info("step6CalculateInterestAccrualAndCheckForLateness '{}'", 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 = 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(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.APPLY_INTEREST,
null,
null,
forDateTime,
MINOR_CURRENCY_UNIT_DIGITS);
if (calculatedLateFee.compareTo(BigDecimal.ZERO) != 0) {
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.MARK_LATE,
null,
null,
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(org.apache.fineract.cn.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 (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());
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 (calculatedLateFee.compareTo(BigDecimal.ZERO) != 0) {
final Set<Debtor> lateFeeDebtors = new HashSet<>();
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,
lateFeeCreditors,
product.getIdentifier(),
customerCase.getIdentifier(),
Action.MARK_LATE);
lateFees = lateFees.add(calculatedLateFee);
productLossAllowance = productLossAllowance.add(provisionForLosses);
}
interestAccrued = interestAccrued.add(calculatedInterest);
updateBalanceMock();
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(org.apache.fineract.cn.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,
final BigDecimal lateFee,
final BigDecimal balanceInEntryAccount,
final String entryAccountIdentifier) throws InterruptedException {
logger.info("step7PaybackPartialAmount '{}' '{}'", amount, forDateTime);
final BigDecimal principal = amount.subtract(interestAccrued).subtract(lateFee.add(nonLateFees));
AccountingFixture.mockBalance(entryAccountIdentifier, balanceInEntryAccount);
final Payment payment = checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.ACCEPT_PAYMENT,
new HashSet<>(Arrays.asList(AccountDesignators.ENTRY, AccountDesignators.CUSTOMER_LOAN_GROUP, AccountDesignators.LOAN_FUNDS_SOURCE)),
amount,
forDateTime,
MINOR_CURRENCY_UNIT_DIGITS,
new CostComponent(ChargeIdentifiers.REPAY_PRINCIPAL_ID, principal),
new CostComponent(ChargeIdentifiers.REPAY_INTEREST_ID, interestAccrued),
new CostComponent(ChargeIdentifiers.REPAY_FEES_ID, lateFee.add(nonLateFees)),
new CostComponent(ChargeIdentifiers.INTEREST_ID, interestAccrued),
new CostComponent(ChargeIdentifiers.LATE_FEE_ID, lateFee));
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.ACCEPT_PAYMENT,
forDateTime,
assignEntry(entryAccountIdentifier),
amount,
IndividualLoanEventConstants.ACCEPT_PAYMENT_INDIVIDUALLOAN_CASE,
Case.State.ACTIVE); //Close has to be done explicitly.
checkNextActionsCorrect(product.getIdentifier(), customerCase.getIdentifier(),
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 customerDepositAccountDebit = principal;
if (interestAccrued.compareTo(BigDecimal.ZERO) != 0) {
customerDepositAccountDebit = customerDepositAccountDebit.add(interestAccrued);
debtors.add(new Debtor(AccountingFixture.LOAN_INTEREST_ACCRUAL_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
}
if (lateFee.add(nonLateFees).compareTo(BigDecimal.ZERO) != 0) {
customerDepositAccountDebit = customerDepositAccountDebit.add(lateFee.add(nonLateFees));
}
if (lateFee.compareTo(BigDecimal.ZERO) != 0) {
debtors.add(new Debtor(AccountingFixture.LATE_FEE_ACCRUAL_ACCOUNT_IDENTIFIER, lateFee.toPlainString()));
}
debtors.add(new Debtor(entryAccountIdentifier, customerDepositAccountDebit.toPlainString()));
final Set<Creditor> creditors = new HashSet<>();
creditors.add(new Creditor(customerLoanPrincipalIdentifier, principal.toPlainString()));
if (interestAccrued.compareTo(BigDecimal.ZERO) != 0) {
creditors.add(new Creditor(customerLoanInterestIdentifier, interestAccrued.toPlainString()));
creditors.add(new Creditor(AccountingFixture.CONSUMER_LOAN_INTEREST_ACCOUNT_IDENTIFIER, interestAccrued.toPlainString()));
}
if (lateFee.add(nonLateFees).compareTo(BigDecimal.ZERO) != 0) {
creditors.add(new Creditor(customerLoanFeeIdentifier, lateFee.add(nonLateFees).toPlainString()));
}
if (lateFee.compareTo(BigDecimal.ZERO) != 0) {
creditors.add(new Creditor(AccountingFixture.LATE_FEE_INCOME_ACCOUNT_IDENTIFIER, lateFee.toPlainString()));
}
AccountingFixture.verifyTransfer(ledgerManager, debtors, creditors, product.getIdentifier(), customerCase.getIdentifier(), Action.ACCEPT_PAYMENT);
expectedCurrentPrincipal = expectedCurrentPrincipal.subtract(principal);
interestAccrued = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
nonLateFees = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
lateFees = BigDecimal.ZERO.setScale(MINOR_CURRENCY_UNIT_DIGITS, RoundingMode.HALF_EVEN);
updateBalanceMock();
logger.info("Completed step7PaybackPartialAmount");
return payment;
}
private void step8Close(
final LocalDateTime forDateTime) throws InterruptedException
{
logger.info("step8Close for '{}'", forDateTime);
checkCostComponentForActionCorrect(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.CLOSE,
null,
null,
forDateTime,
MINOR_CURRENCY_UNIT_DIGITS);
checkStateTransfer(
product.getIdentifier(),
customerCase.getIdentifier(),
Action.CLOSE,
forDateTime,
assignEntry(AccountingFixture.CUSTOMERS_DEPOSIT_ACCOUNT),
IndividualLoanEventConstants.CLOSE_INDIVIDUALLOAN_CASE,
Case.State.CLOSED); //Close has to be done explicitly.
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);
AccountingFixture.mockBalance(customerLoanPrincipalIdentifier, expectedCurrentPrincipal);
AccountingFixture.mockBalance(customerLoanFeeIdentifier, allFees);
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);
}
}