blob: 62ddc51700c3c81dc96edfcf445cd39181537333 [file] [log] [blame]
/*
* Copyright 2017 The Mifos Initiative.
*
* 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;
import io.mifos.core.lang.ServiceException;
import io.mifos.individuallending.api.v1.domain.caseinstance.ChargeName;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPayment;
import io.mifos.individuallending.api.v1.domain.caseinstance.PlannedPaymentPage;
import io.mifos.individuallending.api.v1.domain.workflow.Action;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Nonnull;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
/**
* @author Myrle Krantz
*/
@Service
public class IndividualLoanService {
private final ScheduledChargesService scheduledChargesService;
@Autowired
public IndividualLoanService(final ScheduledChargesService scheduledChargesService) {
this.scheduledChargesService = scheduledChargesService;
}
public PlannedPaymentPage getPlannedPaymentsPage(
final DataContextOfAction dataContextOfAction,
final int pageIndex,
final int size,
final @Nonnull LocalDate initialDisbursalDate) {
final int minorCurrencyUnitDigits = dataContextOfAction.getProductEntity().getMinorCurrencyUnitDigits();
final List<ScheduledAction> scheduledActions = ScheduledActionHelpers.getHypotheticalScheduledActions(initialDisbursalDate, dataContextOfAction.getCaseParameters());
final Set<Action> actionsScheduled = scheduledActions.stream().map(x -> x.action).collect(Collectors.toSet());
final List<ScheduledCharge> scheduledCharges = scheduledChargesService.getScheduledCharges(dataContextOfAction.getProductEntity().getIdentifier(), scheduledActions);
final BigDecimal loanPaymentSize = CostComponentService.getLoanPaymentSize(
dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum(),
dataContextOfAction.getInterest(),
minorCurrencyUnitDigits,
scheduledCharges);
final List<PlannedPayment> plannedPaymentsElements = getPlannedPaymentsElements(
dataContextOfAction.getCaseParametersEntity().getBalanceRangeMaximum(),
minorCurrencyUnitDigits,
actionsScheduled,
scheduledCharges,
loanPaymentSize,
dataContextOfAction.getInterest());
final Set<ChargeName> chargeNames = scheduledCharges.stream()
.map(IndividualLoanService::chargeNameFromChargeDefinition)
.collect(Collectors.toSet());
return constructPage(pageIndex, size, plannedPaymentsElements, chargeNames);
}
private static PlannedPaymentPage constructPage(
final int pageIndex,
final int size,
final List<PlannedPayment> plannedPaymentsElements,
final Set<ChargeName> chargeNames) {
final int fromIndex = size*pageIndex;
final int toIndex = Math.min(size*(pageIndex+1), plannedPaymentsElements.size());
if (toIndex < fromIndex)
throw ServiceException.badRequest("Page number ''{0}'' out of range.", pageIndex);
final List<PlannedPayment> elements = plannedPaymentsElements.subList(fromIndex, toIndex);
final PlannedPaymentPage ret = new PlannedPaymentPage();
ret.setElements(elements);
ret.setChargeNames(chargeNames);
ret.setTotalElements((long) plannedPaymentsElements.size());
final int partialPage = Math.floorMod(plannedPaymentsElements.size(), size) == 0 ? 0 : 1;
ret.setTotalPages(Math.floorDiv(plannedPaymentsElements.size(), size)+ partialPage);
return ret;
}
private static ChargeName chargeNameFromChargeDefinition(final ScheduledCharge scheduledCharge) {
return new ChargeName(scheduledCharge.getChargeDefinition().getIdentifier(), scheduledCharge.getChargeDefinition().getName());
}
static private List<PlannedPayment> getPlannedPaymentsElements(
final BigDecimal initialBalance,
final int minorCurrencyUnitDigits,
final Set<Action> actionsScheduled,
final List<ScheduledCharge> scheduledCharges,
final BigDecimal loanPaymentSize,
final BigDecimal interest) {
final Map<Period, SortedSet<ScheduledCharge>> orderedScheduledChargesGroupedByPeriod
= scheduledCharges.stream()
.filter(scheduledCharge -> chargeIsNotAccruedOrAccruesAtActionScheduled(actionsScheduled, scheduledCharge))
.collect(Collectors.groupingBy(IndividualLoanService::getPeriodFromScheduledCharge,
Collectors.mapping(x -> x,
Collector.of(
() -> new TreeSet<>(new ScheduledChargeComparator()),
SortedSet::add,
(left, right) -> { left.addAll(right); return left; }))));
final List<Period> sortedRepaymentPeriods
= orderedScheduledChargesGroupedByPeriod.keySet().stream()
.sorted()
.collect(Collector.of(ArrayList::new, List::add, (left, right) -> { left.addAll(right); return left; }));
BigDecimal balance = initialBalance.setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN);
final List<PlannedPayment> plannedPayments = new ArrayList<>();
for (int i = 0; i < sortedRepaymentPeriods.size(); i++)
{
final Period repaymentPeriod = sortedRepaymentPeriods.get(i);
final BigDecimal currentLoanPaymentSize;
if (repaymentPeriod.isDefined()) {
// last repayment period: Force the proposed payment to "overhang". Cost component calculation
// corrects last loan payment downwards but not upwards.
if (i == sortedRepaymentPeriods.size() - 1)
currentLoanPaymentSize = loanPaymentSize.add(BigDecimal.valueOf(sortedRepaymentPeriods.size()));
else
currentLoanPaymentSize = loanPaymentSize;
}
else
currentLoanPaymentSize = BigDecimal.ZERO;
final SortedSet<ScheduledCharge> scheduledChargesInPeriod = orderedScheduledChargesGroupedByPeriod.get(repaymentPeriod);
final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
CostComponentService.getCostComponentsForScheduledCharges(
Collections.emptyMap(),
scheduledChargesInPeriod,
initialBalance,
balance,
currentLoanPaymentSize,
interest,
minorCurrencyUnitDigits,
false);
final PlannedPayment plannedPayment = new PlannedPayment();
plannedPayment.setCostComponents(new ArrayList<>(costComponentsForRepaymentPeriod.getCostComponents().values()));
plannedPayment.setDate(repaymentPeriod.getEndDateAsString());
balance = balance.add(costComponentsForRepaymentPeriod.getBalanceAdjustment());
plannedPayment.setRemainingPrincipal(balance);
plannedPayments.add(plannedPayment);
}
return plannedPayments;
}
private static boolean chargeIsNotAccruedOrAccruesAtActionScheduled(
final Set<Action> actionsScheduled,
final ScheduledCharge scheduledCharge) {
// For example to prevent late charges from showing up on planned payments.
return scheduledCharge.getChargeDefinition().getAccrueAction() == null ||
actionsScheduled.contains(Action.valueOf(scheduledCharge.getChargeDefinition().getAccrueAction()));
}
private static Period getPeriodFromScheduledCharge(final ScheduledCharge scheduledCharge) {
final ScheduledAction scheduledAction = scheduledCharge.getScheduledAction();
if (ScheduledActionHelpers.actionHasNoActionPeriod(scheduledAction.action))
return new Period(null, null);
else
return scheduledAction.repaymentPeriod;
}
}