| /* |
| * 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.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.service.AnnuityPayment; |
| import io.mifos.individuallending.internal.service.RateCollectors; |
| import io.mifos.individuallending.internal.service.schedule.Period; |
| import io.mifos.individuallending.internal.service.schedule.ScheduledCharge; |
| import io.mifos.portfolio.api.v1.domain.ChargeDefinition; |
| import org.javamoney.calc.common.Rate; |
| import org.javamoney.moneta.Money; |
| |
| import javax.money.MonetaryAmount; |
| import java.math.BigDecimal; |
| import java.time.Clock; |
| import java.time.LocalDate; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.function.Function; |
| import java.util.stream.Collectors; |
| |
| /** |
| * @author Myrle Krantz |
| */ |
| public class CostComponentService { |
| private static final int EXTRA_PRECISION = 4; |
| private static final int RUNNING_CALCULATION_PRECISION = 8; |
| |
| public static PaymentBuilder getCostComponentsForScheduledCharges( |
| final Collection<ScheduledCharge> scheduledCharges, |
| final BigDecimal maximumBalance, |
| final RunningBalances preChargeBalances, |
| final BigDecimal contractualRepayment, |
| final BigDecimal requestedDisbursement, |
| final BigDecimal requestedRepayment, |
| final BigDecimal percentPoints, |
| final int minorCurrencyUnitDigits, |
| final boolean accrualAccounting) { |
| final PaymentBuilder paymentBuilder = new PaymentBuilder(preChargeBalances, accrualAccounting); |
| |
| for (final ScheduledCharge scheduledCharge : scheduledCharges) { |
| if (accrualAccounting || !isAccrualChargeForAction(scheduledCharge.getChargeDefinition(), scheduledCharge.getScheduledAction().getAction())) { |
| final BigDecimal chargeAmount; |
| if (!isIncurralActionForAccruedCharge(scheduledCharge.getChargeDefinition(), scheduledCharge.getScheduledAction().getAction())) |
| { |
| final BigDecimal amountProportionalTo = getAmountProportionalTo( |
| scheduledCharge, |
| maximumBalance, |
| preChargeBalances, |
| contractualRepayment, |
| requestedDisbursement, |
| requestedRepayment, |
| paymentBuilder); |
| if (scheduledCharge.getChargeRange().map(x -> |
| !x.amountIsWithinRange(amountProportionalTo)).orElse(false)) |
| continue; |
| |
| chargeAmount = howToApplyScheduledChargeToAmount(scheduledCharge, percentPoints) |
| .apply(amountProportionalTo) |
| .setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN); |
| } |
| else |
| { |
| chargeAmount = preChargeBalances.getAccruedBalanceForCharge(scheduledCharge.getChargeDefinition()) |
| .add(paymentBuilder.getBalanceAdjustment(scheduledCharge.getChargeDefinition().getAccrualAccountDesignator())); |
| } |
| |
| paymentBuilder.adjustBalances( |
| scheduledCharge.getScheduledAction().getAction(), |
| scheduledCharge.getChargeDefinition(), |
| chargeAmount); |
| } |
| } |
| |
| return paymentBuilder; |
| } |
| |
| private static BigDecimal getAmountProportionalTo( |
| final ScheduledCharge scheduledCharge, |
| final BigDecimal maximumBalance, |
| final RunningBalances runningBalances, |
| final BigDecimal contractualRepayment, |
| final BigDecimal requestedDisbursement, |
| final BigDecimal requestedRepayment, |
| final PaymentBuilder paymentBuilder) { |
| final Optional<ChargeProportionalDesignator> optionalChargeProportionalTo |
| = ChargeProportionalDesignator.fromString(scheduledCharge.getChargeDefinition().getProportionalTo()); |
| return optionalChargeProportionalTo.map(chargeProportionalTo -> |
| getAmountProportionalTo( |
| scheduledCharge, |
| chargeProportionalTo, |
| maximumBalance, |
| runningBalances, |
| contractualRepayment, |
| requestedDisbursement, |
| requestedRepayment, |
| paymentBuilder)) |
| .orElse(BigDecimal.ZERO); |
| } |
| |
| static BigDecimal getAmountProportionalTo( |
| final ScheduledCharge scheduledCharge, |
| final ChargeProportionalDesignator chargeProportionalTo, |
| final BigDecimal maximumBalance, |
| final RunningBalances runningBalances, |
| final BigDecimal contractualRepayment, |
| final BigDecimal requestedDisbursement, |
| final BigDecimal requestedRepayment, |
| final PaymentBuilder paymentBuilder) { |
| switch (chargeProportionalTo) { |
| case NOT_PROPORTIONAL: |
| return BigDecimal.ONE; |
| case MAXIMUM_BALANCE_DESIGNATOR: |
| return maximumBalance; |
| case RUNNING_BALANCE_DESIGNATOR: { |
| final BigDecimal customerLoanRunningBalance = runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_GROUP); |
| return customerLoanRunningBalance.subtract(paymentBuilder.getBalanceAdjustment(AccountDesignators.CUSTOMER_LOAN_GROUP)); |
| } |
| case PRINCIPAL_DESIGNATOR: { |
| return runningBalances.getBalance(AccountDesignators.CUSTOMER_LOAN_PRINCIPAL); |
| } |
| case CONTRACTUAL_REPAYMENT_DESIGNATOR: |
| return contractualRepayment; |
| case REQUESTED_DISBURSEMENT_DESIGNATOR: |
| return requestedDisbursement; |
| case REQUESTED_REPAYMENT_DESIGNATOR: |
| return requestedRepayment.add(paymentBuilder.getBalanceAdjustment(AccountDesignators.ENTRY)); |
| case TO_ACCOUNT_DESIGNATOR: |
| return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getToAccountDesignator()) |
| .subtract(paymentBuilder.getBalanceAdjustment(scheduledCharge.getChargeDefinition().getToAccountDesignator())); |
| case FROM_ACCOUNT_DESIGNATOR: |
| return runningBalances.getBalance(scheduledCharge.getChargeDefinition().getFromAccountDesignator()) |
| .add(paymentBuilder.getBalanceAdjustment(scheduledCharge.getChargeDefinition().getFromAccountDesignator())); |
| default: |
| return BigDecimal.ZERO; |
| } |
| //TODO: correctly implement charges which are proportional to other charges. |
| } |
| |
| private static Function<BigDecimal, BigDecimal> howToApplyScheduledChargeToAmount( |
| final ScheduledCharge scheduledCharge, final BigDecimal percentPoints) |
| { |
| switch (scheduledCharge.getChargeDefinition().getChargeMethod()) |
| { |
| case FIXED: { |
| return (amountProportionalTo) -> scheduledCharge.getChargeDefinition().getAmount(); |
| } |
| case PROPORTIONAL: { |
| final BigDecimal chargeAmountPerPeriod = PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, scheduledCharge.getChargeDefinition().getAmount(), RUNNING_CALCULATION_PRECISION); |
| return chargeAmountPerPeriod::multiply; |
| } |
| case INTEREST: { |
| final BigDecimal chargeAmountPerPeriod = PeriodChargeCalculator.chargeAmountPerPeriod(scheduledCharge, percentPoints, RUNNING_CALCULATION_PRECISION); |
| return chargeAmountPerPeriod::multiply; |
| } |
| default: { |
| return (amountProportionalTo) -> BigDecimal.ZERO; |
| } |
| } |
| } |
| |
| public static BigDecimal getLoanPaymentSize( |
| final BigDecimal maximumBalanceSize, |
| final BigDecimal disbursementSize, |
| final BigDecimal interest, |
| final int minorCurrencyUnitDigits, |
| final List<ScheduledCharge> scheduledCharges) { |
| final int precision = disbursementSize.precision() - disbursementSize.scale() + minorCurrencyUnitDigits + EXTRA_PRECISION; |
| final Map<Period, BigDecimal> accrualRatesByPeriod |
| = PeriodChargeCalculator.getPeriodAccrualInterestRate(interest, scheduledCharges, precision); |
| |
| final int periodCount = accrualRatesByPeriod.size(); |
| if (periodCount == 0) |
| return disbursementSize; |
| |
| final BigDecimal geometricMeanAccrualRate = accrualRatesByPeriod.values().stream() |
| .collect(RateCollectors.geometricMean(precision)); |
| |
| final List<ScheduledCharge> disbursementFees = scheduledCharges.stream() |
| .filter(x -> x.getScheduledAction().getAction().equals(Action.DISBURSE)) |
| .collect(Collectors.toList()); |
| final PaymentBuilder paymentBuilder = getCostComponentsForScheduledCharges( |
| disbursementFees, |
| maximumBalanceSize, |
| new SimulatedRunningBalances(), |
| BigDecimal.ZERO, //Contractual repayment not determined yet here. |
| disbursementSize, |
| BigDecimal.ZERO, |
| interest, |
| minorCurrencyUnitDigits, |
| false |
| ); |
| final BigDecimal finalDisbursementSize = paymentBuilder.getBalanceAdjustment( |
| AccountDesignators.CUSTOMER_LOAN_PRINCIPAL, |
| AccountDesignators.CUSTOMER_LOAN_FEES).negate(); |
| |
| final MonetaryAmount presentValue = AnnuityPayment.calculate( |
| Money.of(finalDisbursementSize, "XXX"), |
| Rate.of(geometricMeanAccrualRate), |
| periodCount); |
| return BigDecimal.valueOf(presentValue.getNumber().doubleValueExact()).setScale(minorCurrencyUnitDigits, BigDecimal.ROUND_HALF_EVEN); |
| } |
| |
| private static boolean isIncurralActionForAccruedCharge(final ChargeDefinition chargeDefinition, final Action action) { |
| return chargeDefinition.getAccrueAction() != null && |
| chargeDefinition.getChargeAction().equals(action.name()); |
| } |
| |
| private static boolean isAccrualChargeForAction(final ChargeDefinition chargeDefinition, final Action action) { |
| return chargeDefinition.getAccrueAction() != null && |
| chargeDefinition.getAccrueAction().equals(action.name()); |
| } |
| |
| public static LocalDate today() { |
| return LocalDate.now(Clock.systemUTC()); |
| } |
| |
| } |