/*
* Copyright (c) 2005-2011 Grameen Foundation USA
* All rights reserved.
*
* 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.
*
* See also http://www.apache.org/licenses/LICENSE-2.0.html for an
* explanation of the license and how it is applied.
*/
package org.mifos.accounts.loan.business;
import static java.math.BigDecimal.valueOf;
import static java.util.Arrays.asList;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.mifos.framework.TestUtils.getDate;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mifos.accounts.business.AccountActionEntity;
import org.mifos.accounts.business.AccountPaymentEntity;
import org.mifos.accounts.loan.persistance.LegacyLoanDao;
import org.mifos.accounts.loan.schedule.calculation.ScheduleCalculator;
import org.mifos.accounts.loan.schedule.domain.Installment;
import org.mifos.accounts.loan.schedule.domain.InstallmentBuilder;
import org.mifos.accounts.loan.schedule.domain.Schedule;
import org.mifos.accounts.loan.schedule.domain.ScheduleMatcher;
import org.mifos.accounts.util.helpers.PaymentStatus;
import org.mifos.application.master.business.MifosCurrency;
import org.mifos.config.business.service.ConfigurationBusinessService;
import org.mifos.customers.personnel.business.PersonnelBO;
import org.mifos.framework.exceptions.PersistenceException;
import org.mifos.framework.util.CollectionUtils;
import org.mifos.framework.util.helpers.Money;
import org.mifos.framework.util.helpers.Transformer;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class ScheduleCalculatorAdaptorTest {
@Mock
private ScheduleCalculatorAdaptor scheduleCalculatorAdaptor;
@Mock
private ScheduleMapper scheduleMapper;
@Mock
private LoanBO loanBO;
@Mock
private PersonnelBO personnel;
@Mock
private AccountPaymentEntity accountPaymentEntity;
@Mock
private LegacyLoanDao legacyLoanDao;
@Mock
private AccountActionEntity accountActionEntity;
@Mock
private LoanSummaryEntity loanSummary;
@Mock
private LoanPerformanceHistoryEntity performanceHistory;
@Mock
private ConfigurationBusinessService configurationBusinessService;
@Mock
private ScheduleCalculator scheduleCalculator;
private MifosCurrency rupee = new MifosCurrency(Short.valueOf("1"), "Rupee", valueOf(1), "INR");
private static final Double DAILY_INTEREST_RATE = 0.000658;
private static final Double ANNUAL_INTEREST_RATE = 24.0;
private static BigDecimal LOAN_AMOUNT = new BigDecimal(1000);
private static final Date DISBURSEMENT_DATE = getDate(23, 9, 2010);
@Before
public void setup() {
scheduleCalculator = Mockito.spy(new ScheduleCalculator());
scheduleMapper = Mockito.spy(new ScheduleMapper());
scheduleCalculatorAdaptor = new ScheduleCalculatorAdaptor(scheduleCalculator, scheduleMapper, configurationBusinessService);
LOAN_AMOUNT = LOAN_AMOUNT.setScale(13, RoundingMode.HALF_UP);
when(loanBO.getCurrency()).thenReturn(rupee);
}
@Test
public void shouldApplyPaymentZeroPayment() throws PersistenceException {
Set<LoanScheduleEntity> loanScheduleEntities = getLoanScheduleEntities();
when(loanBO.getLoanScheduleEntities()).thenReturn(loanScheduleEntities);
when(loanBO.getDisbursementDate()).thenReturn(DISBURSEMENT_DATE);
when(loanBO.getLoanAmount()).thenReturn(new Money(rupee, LOAN_AMOUNT));
when(loanBO.getInterestRate()).thenReturn(ANNUAL_INTEREST_RATE);
when(loanBO.getlegacyLoanDao()).thenReturn(legacyLoanDao);
when(loanBO.getLoanSummary()).thenReturn(loanSummary);
when(loanBO.getPerformanceHistory()).thenReturn(performanceHistory);
when(accountPaymentEntity.getAccount()).thenReturn(loanBO);
scheduleCalculatorAdaptor.applyPayment(loanBO, Money.zero(rupee), getDate(30, 10, 2010), personnel, accountPaymentEntity, false);
verify(scheduleMapper, times(1)).mapToSchedule(Mockito.<Collection<LoanScheduleEntity>>any(), Mockito.<Date>any(), Mockito.<Double>any(), Mockito.<BigDecimal>any());
verify(scheduleCalculator).applyPayment(Mockito.<Schedule>any(), Mockito.<BigDecimal>any(), Mockito.<Date>any(), Mockito.anyBoolean());
verify(scheduleMapper).populatePaymentDetails(Mockito.<Schedule>any(), Mockito.<LoanBO>any(), Mockito.<Date>any(), Mockito.<PersonnelBO>any(), Mockito.<AccountPaymentEntity>any());
verify(loanBO, times(2)).getLoanScheduleEntities();
verify(loanBO, times(1)).getDisbursementDate();
verify(loanBO, times(1)).getInterestRate();
verify(loanBO, times(0)).getlegacyLoanDao();
verify(loanBO, times(0)).getLoanSummary();
verify(loanBO, times(0)).getPerformanceHistory();
verify(accountPaymentEntity, times(0)).getAccount();
verify(legacyLoanDao, times(0)).getPersistentObject(eq(AccountActionEntity.class), Mockito.<Serializable>any());
verify(loanSummary, times(0)).updatePaymentDetails(Mockito.<PaymentAllocation>any());
verify(performanceHistory, times(0)).incrementPayments();
}
@Test
public void shouldApplyPayment() throws PersistenceException {
Set<LoanScheduleEntity> loanScheduleEntities = getLoanScheduleEntities();
when(loanBO.getLoanScheduleEntities()).thenReturn(loanScheduleEntities);
when(loanBO.getDisbursementDate()).thenReturn(DISBURSEMENT_DATE);
when(loanBO.getLoanAmount()).thenReturn(new Money(rupee, LOAN_AMOUNT));
when(loanBO.getInterestRate()).thenReturn(ANNUAL_INTEREST_RATE);
when(loanBO.getlegacyLoanDao()).thenReturn(legacyLoanDao);
when(accountPaymentEntity.getAccount()).thenReturn(loanBO);
scheduleCalculatorAdaptor.applyPayment(loanBO, new Money(rupee, 112.00), getDate(30, 10, 2010), personnel, accountPaymentEntity, false);
verify(scheduleMapper, times(1)).mapToSchedule(Mockito.<Collection<LoanScheduleEntity>>any(), Mockito.<Date>any(), Mockito.<Double>any(), Mockito.<BigDecimal>any());
verify(scheduleCalculator).applyPayment(Mockito.<Schedule>any(), Mockito.<BigDecimal>any(), Mockito.<Date>any(), Mockito.anyBoolean());
verify(scheduleMapper).populatePaymentDetails(Mockito.<Schedule>any(), Mockito.<LoanBO>any(), Mockito.<Date>any(), Mockito.<PersonnelBO>any(), Mockito.<AccountPaymentEntity>any());
verify(loanBO, times(2)).getLoanScheduleEntities();
verify(loanBO, times(1)).getDisbursementDate();
verify(loanBO, times(1)).getInterestRate();
verify(loanBO, times(2)).getlegacyLoanDao();
verify(loanBO, times(2)).recordSummaryAndPerfHistory(anyBoolean(), Matchers.<PaymentAllocation>any());
verify(accountPaymentEntity, times(2)).getAccount();
}
@Test
public void shouldComputeExtraInterestForDecliningPrincipalBalance() {
Set<LoanScheduleEntity> loanScheduleEntities = getLoanScheduleEntities();
when(loanBO.isDecliningBalanceInterestRecalculation()).thenReturn(true);
when(loanBO.getLoanScheduleEntities()).thenReturn(loanScheduleEntities);
when(loanBO.getDisbursementDate()).thenReturn(DISBURSEMENT_DATE);
when(loanBO.getLoanAmount()).thenReturn(new Money(rupee, LOAN_AMOUNT));
when(loanBO.getInterestRate()).thenReturn(ANNUAL_INTEREST_RATE);
when(loanBO.getLoanScheduleEntityMap()).thenReturn(getLoanScheduleEntityMap(loanScheduleEntities));
scheduleCalculatorAdaptor.computeExtraInterest(loanBO, getDate(30, 10, 2010));
Schedule expectedSchedule = getSchedule(DISBURSEMENT_DATE, LOAN_AMOUNT, getInstallments(0, .46, 0));
verify(scheduleCalculator).computeExtraInterest(argThat(new ScheduleMatcher(expectedSchedule)), Mockito.eq(getDate(30, 10, 2010)));
verify(loanBO, times(1)).isDecliningBalanceInterestRecalculation();
verify(loanBO, times(1)).getLoanScheduleEntities();
verify(loanBO, times(1)).getDisbursementDate();
verify(loanBO, times(1)).getLoanAmount();
verify(loanBO, times(1)).getInterestRate();
verify(loanBO, times(1)).getLoanScheduleEntityMap();
ArrayList<LoanScheduleEntity> loanScheduleEntitiesWithExtraInterest = new ArrayList<LoanScheduleEntity>(loanBO.getLoanScheduleEntities());
assertExtraInterest(loanScheduleEntitiesWithExtraInterest.get(0), 0.0);
assertExtraInterest(loanScheduleEntitiesWithExtraInterest.get(1), 0.46);
assertExtraInterest(loanScheduleEntitiesWithExtraInterest.get(2), 0.0);
}
@Test
public void shouldNotComputeExtraInterestForNonPrincipalBalanceInterestTypes() {
Set<LoanScheduleEntity> loanScheduleEntities = getLoanScheduleEntities();
when(loanBO.isDecliningBalanceInterestRecalculation()).thenReturn(false);
when(loanBO.getLoanScheduleEntities()).thenReturn(loanScheduleEntities);
when(loanBO.getDisbursementDate()).thenReturn(DISBURSEMENT_DATE);
when(loanBO.getLoanAmount()).thenReturn(new Money(rupee, LOAN_AMOUNT));
when(loanBO.getInterestRate()).thenReturn(ANNUAL_INTEREST_RATE);
when(loanBO.getLoanScheduleEntityMap()).thenReturn(getLoanScheduleEntityMap(loanScheduleEntities));
scheduleCalculatorAdaptor.computeExtraInterest(loanBO, getDate(30, 10, 2010));
verify(scheduleCalculator, times(0)).computeExtraInterest(Mockito.<Schedule>any(), Mockito.<Date>any());
verify(loanBO, times(0)).getInterestRate();
verify(loanBO, times(0)).getLoanScheduleEntities();
verify(loanBO, times(0)).getDisbursementDate();
verify(loanBO, times(0)).getLoanAmount();
verify(loanBO, times(0)).getInterestRate();
verify(loanBO, times(0)).getLoanScheduleEntityMap();
ArrayList<LoanScheduleEntity> loanScheduleEntitiesWithExtraInterest = new ArrayList<LoanScheduleEntity>(loanBO.getLoanScheduleEntities());
assertExtraInterest(loanScheduleEntitiesWithExtraInterest.get(0), 0.0);
assertExtraInterest(loanScheduleEntitiesWithExtraInterest.get(1), 0.0);
assertExtraInterest(loanScheduleEntitiesWithExtraInterest.get(2), 0.0);
}
@Test
public void computeExtraInterestAndPopulateInLoanScheduleEntities() {
List<Installment> installments = getInstallments(0, 0, 0);
Schedule schedule = new Schedule(DISBURSEMENT_DATE, DAILY_INTEREST_RATE, LOAN_AMOUNT, installments);
new ScheduleCalculator().computeExtraInterest(schedule, getDate(30, 10, 2010));
Set<LoanScheduleEntity> loanScheduleEntities = getLoanScheduleEntities();
Map<Integer, LoanScheduleEntity> loanScheduleEntityMap = getLoanScheduleEntityMap(loanScheduleEntities);
assertThat(schedule.getInstallments().get(2).getExtraInterest().doubleValue(), is(0.46));
scheduleCalculatorAdaptor.populateExtraInterestInLoanScheduleEntities(schedule, loanScheduleEntityMap);
for (Installment installment : installments) {
LoanScheduleEntity loanScheduleEntity = loanScheduleEntityMap.get(installment.getId());
assertExtraInterest(loanScheduleEntity, installment.getExtraInterest().doubleValue());
}
}
@Test
public void shouldComputeRepaymentAmount() {
Set<LoanScheduleEntity> loanScheduleEntities = getLoanScheduleEntities();
when(loanBO.getLoanScheduleEntities()).thenReturn(loanScheduleEntities);
when(loanBO.getDisbursementDate()).thenReturn(DISBURSEMENT_DATE);
when(loanBO.getLoanAmount()).thenReturn(new Money(rupee, LOAN_AMOUNT));
when(loanBO.getInterestRate()).thenReturn(ANNUAL_INTEREST_RATE);
Date asOfDate = getDate(30, 10, 2010);
scheduleCalculatorAdaptor.computeRepaymentAmount(loanBO, asOfDate);
verify(scheduleMapper, times(1)).mapToSchedule(Mockito.<Collection<LoanScheduleEntity>>any(), Mockito.<Date>any(), Mockito.<Double>any(), Mockito.<BigDecimal>any());
verify(scheduleCalculator).computeRepaymentAmount(Mockito.<Schedule>any(), eq(asOfDate));
verify(loanBO).getLoanScheduleEntities();
verify(loanBO).getDisbursementDate();
verify(loanBO).getInterestRate();
}
private Schedule getSchedule(Date disbursementDate, BigDecimal loanAmount, List<Installment> installments) {
return new Schedule(disbursementDate, DAILY_INTEREST_RATE, loanAmount, installments);
}
private void assertExtraInterest(LoanScheduleEntity loanScheduleEntity, double extraInterest) {
assertThat(loanScheduleEntity.getExtraInterest().getAmount().doubleValue(), is(extraInterest));
}
private Map<Integer, LoanScheduleEntity> getLoanScheduleEntityMap(Collection<LoanScheduleEntity> loanScheduleEntities) {
return CollectionUtils.asValueMap(loanScheduleEntities, new Transformer<LoanScheduleEntity, Integer>() {
@Override
public Integer transform(LoanScheduleEntity input) {
return Integer.valueOf(input.getInstallmentId());
}
});
}
private List<Installment> getInstallments(double... extraInterest) {
return asList(
getInstallment(1, getDate(23, 10, 2010), getDate(23, 10, 2010), new double[]{100, 10, extraInterest[0], 0.0, 0.0, 0.0, 0.0}, new double[]{0, 0, 0.0, 0.0, 0.0, 0.0, 0.0}),
getInstallment(2, getDate(23, 11, 2010), getDate(23, 11, 2010), new double[]{100, 10, extraInterest[1], 0.0, 0.0, 0.0, 0.0}, new double[]{0, 0, 0.0, 0.0, 0.0, 0.0, 0.0}),
getInstallment(3, getDate(23, 12, 2010), getDate(23, 12, 2010), new double[]{100, 10, extraInterest[2], 0.0, 0.0, 0.0, 0.0}, new double[]{0, 0, 0.0, 0.0, 0.0, 0.0, 0.0})
);
}
private Installment getInstallment(int id, Date dueDate, Date paidDate, double[] actualAmounts, double[] paidAmounts) {
return new InstallmentBuilder(String.valueOf(id)).
withDueDate(dueDate).
withPaymentDate(paidDate).
withPrincipal(actualAmounts[0]).withPrincipalPaid(paidAmounts[0]).
withInterest(actualAmounts[1]).withInterestPaid(paidAmounts[1]).
withExtraInterest(actualAmounts[2]).withExtraInterestPaid(paidAmounts[2]).
withFees(actualAmounts[3]).withFeesPaid(paidAmounts[3]).
withMiscFees(actualAmounts[4]).withMiscFeesPaid(paidAmounts[4]).
withPenalty(actualAmounts[5]).withPenaltyPaid(paidAmounts[5]).
withMiscPenalty(actualAmounts[6]).withMiscPenaltyPaid(paidAmounts[6]).
build();
}
private Set<LoanScheduleEntity> getLoanScheduleEntities() {
LinkedHashSet<LoanScheduleEntity> loanScheduleEntities = new LinkedHashSet<LoanScheduleEntity>();
loanScheduleEntities.add(new LoanScheduleBuilder("1", loanBO).withDueDate(getDate(23, 10, 2010)).
withPaymentStatus(PaymentStatus.UNPAID).withPrincipal(100).withInterest(10).build());
loanScheduleEntities.add(new LoanScheduleBuilder("2", loanBO).withDueDate(getDate(23, 11, 2010)).
withPaymentStatus(PaymentStatus.UNPAID).withPrincipal(100).withInterest(10).build());
loanScheduleEntities.add(new LoanScheduleBuilder("3", loanBO).withDueDate(getDate(23, 12, 2010)).
withPaymentStatus(PaymentStatus.UNPAID).withPrincipal(100).withInterest(10).build());
return loanScheduleEntities;
}
}