org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler.java

Source

/**
 * 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.portfolio.loanaccount.loanschedule.service;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.lang.StringUtils;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
import org.apache.fineract.organisation.holiday.domain.HolidayStatusType;
import org.apache.fineract.organisation.holiday.service.HolidayUtil;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil;
import org.apache.fineract.portfolio.accountdetails.domain.AccountType;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType;
import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository;
import org.apache.fineract.portfolio.calendar.domain.CalendarRepository;
import org.apache.fineract.portfolio.calendar.domain.CalendarType;
import org.apache.fineract.portfolio.calendar.exception.CalendarNotFoundException;
import org.apache.fineract.portfolio.calendar.exception.MeetingFrequencyMismatchException;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.common.domain.DayOfWeekType;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.floatingrates.data.FloatingRateDTO;
import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData;
import org.apache.fineract.portfolio.floatingrates.exception.FloatingRateNotFoundException;
import org.apache.fineract.portfolio.floatingrates.service.FloatingRatesReadPlatformService;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationsComparator;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.apache.fineract.portfolio.loanaccount.exception.MinDaysBetweenDisbursalAndFirstRepaymentViolationException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductInterestRecalculationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductVariableInstallmentConfig;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.useradministration.domain.AppUser;
import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

@Service
public class LoanScheduleAssembler {

    private final FromJsonHelper fromApiJsonHelper;
    private final LoanProductRepository loanProductRepository;
    private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository;
    private final LoanChargeAssembler loanChargeAssembler;
    private final LoanScheduleGeneratorFactory loanScheduleFactory;
    private final AprCalculator aprCalculator;
    private final CalendarRepository calendarRepository;
    private final HolidayRepository holidayRepository;
    private final ConfigurationDomainService configurationDomainService;
    private final ClientRepositoryWrapper clientRepository;
    private final GroupRepositoryWrapper groupRepository;
    private final WorkingDaysRepositoryWrapper workingDaysRepository;
    private final FloatingRatesReadPlatformService floatingRatesReadPlatformService;
    private final VariableLoanScheduleFromApiJsonValidator variableLoanScheduleFromApiJsonValidator;
    private final CalendarInstanceRepository calendarInstanceRepository;
    private final PlatformSecurityContext context;
    private final LoanUtilService loanUtilService;

    @Autowired
    public LoanScheduleAssembler(final FromJsonHelper fromApiJsonHelper,
            final LoanProductRepository loanProductRepository,
            final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository,
            final LoanScheduleGeneratorFactory loanScheduleFactory, final AprCalculator aprCalculator,
            final LoanChargeAssembler loanChargeAssembler, final CalendarRepository calendarRepository,
            final HolidayRepository holidayRepository, final ConfigurationDomainService configurationDomainService,
            final ClientRepositoryWrapper clientRepository, final GroupRepositoryWrapper groupRepository,
            final WorkingDaysRepositoryWrapper workingDaysRepository,
            final FloatingRatesReadPlatformService floatingRatesReadPlatformService,
            final VariableLoanScheduleFromApiJsonValidator variableLoanScheduleFromApiJsonValidator,
            final CalendarInstanceRepository calendarInstanceRepository, final PlatformSecurityContext context,
            final LoanUtilService loanUtilService) {
        this.fromApiJsonHelper = fromApiJsonHelper;
        this.loanProductRepository = loanProductRepository;
        this.applicationCurrencyRepository = applicationCurrencyRepository;
        this.loanScheduleFactory = loanScheduleFactory;
        this.aprCalculator = aprCalculator;
        this.loanChargeAssembler = loanChargeAssembler;
        this.calendarRepository = calendarRepository;
        this.holidayRepository = holidayRepository;
        this.configurationDomainService = configurationDomainService;
        this.clientRepository = clientRepository;
        this.groupRepository = groupRepository;
        this.workingDaysRepository = workingDaysRepository;
        this.floatingRatesReadPlatformService = floatingRatesReadPlatformService;
        this.variableLoanScheduleFromApiJsonValidator = variableLoanScheduleFromApiJsonValidator;
        this.calendarInstanceRepository = calendarInstanceRepository;
        this.context = context;
        this.loanUtilService = loanUtilService;
    }

    public LoanApplicationTerms assembleLoanTerms(final JsonElement element) {
        final Long loanProductId = this.fromApiJsonHelper.extractLongNamed("productId", element);

        final LoanProduct loanProduct = this.loanProductRepository.findOne(loanProductId);
        if (loanProduct == null) {
            throw new LoanProductNotFoundException(loanProductId);
        }

        return assembleLoanApplicationTermsFrom(element, loanProduct);
    }

    private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement element,
            final LoanProduct loanProduct) {

        final MonetaryCurrency currency = loanProduct.getCurrency();
        final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository
                .findOneWithNotFoundDetection(currency);

        // loan terms
        final Integer loanTermFrequency = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("loanTermFrequency",
                element);
        final Integer loanTermFrequencyType = this.fromApiJsonHelper
                .extractIntegerWithLocaleNamed("loanTermFrequencyType", element);
        final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(loanTermFrequencyType);

        final Integer numberOfRepayments = this.fromApiJsonHelper
                .extractIntegerWithLocaleNamed("numberOfRepayments", element);
        final Integer repaymentEvery = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentEvery",
                element);
        final Integer repaymentFrequencyType = this.fromApiJsonHelper
                .extractIntegerWithLocaleNamed("repaymentFrequencyType", element);
        final PeriodFrequencyType repaymentPeriodFrequencyType = PeriodFrequencyType
                .fromInt(repaymentFrequencyType);
        final Integer nthDay = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("repaymentFrequencyNthDayType",
                element);
        final Integer dayOfWeek = this.fromApiJsonHelper
                .extractIntegerWithLocaleNamed("repaymentFrequencyDayOfWeekType", element);
        final DayOfWeekType weekDayType = DayOfWeekType.fromInt(dayOfWeek);

        final Integer amortizationType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("amortizationType",
                element);
        final AmortizationMethod amortizationMethod = AmortizationMethod.fromInt(amortizationType);

        // interest terms
        final Integer interestType = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("interestType", element);
        final InterestMethod interestMethod = InterestMethod.fromInt(interestType);

        final Integer interestCalculationPeriodType = this.fromApiJsonHelper
                .extractIntegerWithLocaleNamed("interestCalculationPeriodType", element);
        final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod
                .fromInt(interestCalculationPeriodType);
        Boolean allowPartialPeriodInterestCalcualtion = this.fromApiJsonHelper
                .extractBooleanNamed(LoanProductConstants.allowPartialPeriodInterestCalcualtionParamName, element);
        if (allowPartialPeriodInterestCalcualtion == null) {
            allowPartialPeriodInterestCalcualtion = loanProduct.getLoanProductRelatedDetail()
                    .isAllowPartialPeriodInterestCalcualtion();
        }

        final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper
                .extractBigDecimalWithLocaleNamed("interestRatePerPeriod", element);
        final PeriodFrequencyType interestRatePeriodFrequencyType = loanProduct.getInterestPeriodFrequencyType();

        BigDecimal annualNominalInterestRate = BigDecimal.ZERO;
        if (interestRatePerPeriod != null) {
            annualNominalInterestRate = this.aprCalculator.calculateFrom(interestRatePeriodFrequencyType,
                    interestRatePerPeriod);
        }

        // disbursement details
        final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("principal", element);
        final Money principalMoney = Money.of(currency, principal);

        final LocalDate expectedDisbursementDate = this.fromApiJsonHelper
                .extractLocalDateNamed("expectedDisbursementDate", element);
        final LocalDate repaymentsStartingFromDate = this.fromApiJsonHelper
                .extractLocalDateNamed("repaymentsStartingFromDate", element);
        LocalDate calculatedRepaymentsStartingFromDate = repaymentsStartingFromDate;

        final Boolean synchDisbursement = this.fromApiJsonHelper.extractBooleanNamed("syncDisbursementWithMeeting",
                element);
        final Long calendarId = this.fromApiJsonHelper.extractLongNamed("calendarId", element);
        Calendar calendar = null;

        final String loanTypeParameterName = "loanType";
        final String loanTypeStr = this.fromApiJsonHelper.extractStringNamed(loanTypeParameterName, element);

        final AccountType loanType = AccountType.fromName(loanTypeStr);

        /*
         * If it is JLG loan/Group Loan then make sure loan frequency is same as
         * Group/Center meeting frequency or multiple of it. TODO: Check should
         * be either same frequency or loan freq is multiple of center/group
         * meeting freq multiples
         */
        if ((loanType.isJLGAccount() || loanType.isGroupAccount()) && calendarId != null) {
            calendar = this.calendarRepository.findOne(calendarId);
            if (calendar == null) {
                throw new CalendarNotFoundException(calendarId);
            }
            final PeriodFrequencyType meetingPeriodFrequency = CalendarUtils
                    .getMeetingPeriodFrequencyType(calendar.getRecurrence());
            validateRepaymentFrequencyIsSameAsMeetingFrequency(meetingPeriodFrequency.getValue(),
                    repaymentFrequencyType, CalendarUtils.getInterval(calendar.getRecurrence()), repaymentEvery);
        }

        /*
         * If user has not passed the first repayments date then then derive the
         * same based on loan type.
         */
        if (calculatedRepaymentsStartingFromDate == null) {
            calculatedRepaymentsStartingFromDate = deriveFirstRepaymentDate(loanType, repaymentEvery,
                    expectedDisbursementDate, repaymentPeriodFrequencyType,
                    loanProduct.getMinimumDaysBetweenDisbursalAndFirstRepayment(), calendar);
        }

        /*
         * If it is JLG loan/Group Loan synched with a meeting, then make sure
         * first repayment falls on meeting date
         */
        if ((loanType.isJLGAccount() || loanType.isGroupAccount()) && calendar != null) {
            validateRepaymentsStartDateWithMeetingDates(calculatedRepaymentsStartingFromDate, calendar);
            /*
             * If disbursement is synced on meeting, make sure disbursement date
             * is on a meeting date
             */
            if (synchDisbursement != null && synchDisbursement.booleanValue()) {
                validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar);
            }
        }

        validateMinimumDaysBetweenDisbursalAndFirstRepayment(expectedDisbursementDate,
                calculatedRepaymentsStartingFromDate,
                loanProduct.getMinimumDaysBetweenDisbursalAndFirstRepayment());

        // grace details
        final Integer graceOnPrincipalPayment = this.fromApiJsonHelper
                .extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element);
        final Integer graceOnInterestPayment = this.fromApiJsonHelper
                .extractIntegerWithLocaleNamed("graceOnInterestPayment", element);
        final Integer graceOnInterestCharged = this.fromApiJsonHelper
                .extractIntegerWithLocaleNamed("graceOnInterestCharged", element);
        final LocalDate interestChargedFromDate = this.fromApiJsonHelper
                .extractLocalDateNamed("interestChargedFromDate", element);

        final Integer graceOnArrearsAgeing = this.fromApiJsonHelper
                .extractIntegerWithLocaleNamed(LoanProductConstants.graceOnArrearsAgeingParameterName, element);

        // other
        final BigDecimal inArrearsTolerance = this.fromApiJsonHelper
                .extractBigDecimalWithLocaleNamed("inArrearsTolerance", element);
        final Money inArrearsToleranceMoney = Money.of(currency, inArrearsTolerance);

        final BigDecimal emiAmount = this.fromApiJsonHelper
                .extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName, element);
        final BigDecimal maxOutstandingBalance = this.fromApiJsonHelper
                .extractBigDecimalWithLocaleNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element);

        final List<DisbursementData> disbursementDatas = fetchDisbursementData(element.getAsJsonObject());

        /**
         * Interest recalculation settings copy from product definition
         */
        final DaysInMonthType daysInMonthType = loanProduct.fetchDaysInMonthType();

        final DaysInYearType daysInYearType = loanProduct.fetchDaysInYearType();

        final boolean isInterestRecalculationEnabled = loanProduct.isInterestRecalculationEnabled();
        RecalculationFrequencyType recalculationFrequencyType = null;
        CalendarInstance restCalendarInstance = null;
        RecalculationFrequencyType compoundingFrequencyType = null;
        CalendarInstance compoundingCalendarInstance = null;
        if (isInterestRecalculationEnabled) {
            LoanProductInterestRecalculationDetails loanProductInterestRecalculationDetails = loanProduct
                    .getProductInterestRecalculationDetails();
            recalculationFrequencyType = loanProductInterestRecalculationDetails.getRestFrequencyType();
            if (recalculationFrequencyType.isSameAsRepayment()) {
                restCalendarInstance = createCalendarForSameAsRepayment(repaymentEvery,
                        repaymentPeriodFrequencyType, expectedDisbursementDate);
            } else {
                LocalDate calendarStartDate = this.fromApiJsonHelper.extractLocalDateNamed(
                        LoanProductConstants.recalculationRestFrequencyDateParamName, element);
                if (calendarStartDate == null) {
                    calendarStartDate = expectedDisbursementDate;
                }
                restCalendarInstance = createInterestRecalculationCalendarInstance(calendarStartDate,
                        recalculationFrequencyType, loanProductInterestRecalculationDetails.getRestInterval());
            }
            InterestRecalculationCompoundingMethod compoundingMethod = InterestRecalculationCompoundingMethod
                    .fromInt(loanProductInterestRecalculationDetails.getInterestRecalculationCompoundingMethod());
            if (compoundingMethod.isCompoundingEnabled()) {
                compoundingFrequencyType = loanProductInterestRecalculationDetails.getCompoundingFrequencyType();
                if (compoundingFrequencyType.isSameAsRepayment()) {
                    compoundingCalendarInstance = createCalendarForSameAsRepayment(repaymentEvery,
                            repaymentPeriodFrequencyType, expectedDisbursementDate);
                } else {
                    LocalDate calendarStartDate = this.fromApiJsonHelper.extractLocalDateNamed(
                            LoanProductConstants.recalculationCompoundingFrequencyDateParamName, element);
                    if (calendarStartDate == null) {
                        calendarStartDate = expectedDisbursementDate;
                    }
                    compoundingCalendarInstance = createInterestRecalculationCalendarInstance(calendarStartDate,
                            compoundingFrequencyType,
                            loanProductInterestRecalculationDetails.getCompoundingInterval());
                }

            }
        }

        final BigDecimal principalThresholdForLastInstalment = loanProduct
                .getPrincipalThresholdForLastInstallment();

        final Integer installmentAmountInMultiplesOf = loanProduct.getInstallmentAmountInMultiplesOf();

        List<LoanTermVariationsData> loanTermVariations = new ArrayList<>();
        if (loanProduct.isLinkedToFloatingInterestRate()) {
            final BigDecimal interestRateDiff = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
                    LoanApiConstants.interestRateDifferentialParameterName, element);
            final Boolean isFloatingInterestRate = this.fromApiJsonHelper
                    .extractBooleanNamed(LoanApiConstants.isFloatingInterestRateParameterName, element);
            List<FloatingRatePeriodData> baseLendingRatePeriods = null;
            try {
                baseLendingRatePeriods = this.floatingRatesReadPlatformService.retrieveBaseLendingRate()
                        .getRatePeriods();
            } catch (final FloatingRateNotFoundException ex) {
                // Do not do anything
            }
            FloatingRateDTO floatingRateDTO = new FloatingRateDTO(isFloatingInterestRate, expectedDisbursementDate,
                    interestRateDiff, baseLendingRatePeriods);
            Collection<FloatingRatePeriodData> applicableRates = loanProduct.fetchInterestRates(floatingRateDTO);

            LocalDate interestRateStartDate = DateUtils.getLocalDateOfTenant();
            final LocalDate dateValue = null;
            final boolean isSpecificToInstallment = false;
            for (FloatingRatePeriodData periodData : applicableRates) {
                LoanTermVariationsData loanTermVariation = new LoanTermVariationsData(
                        LoanEnumerations.loanvariationType(LoanTermVariationType.INTEREST_RATE),
                        periodData.getFromDateAsLocalDate(), periodData.getInterestRate(), dateValue,
                        isSpecificToInstallment);
                if (!interestRateStartDate.isBefore(periodData.getFromDateAsLocalDate())) {
                    interestRateStartDate = periodData.getFromDateAsLocalDate();
                    annualNominalInterestRate = periodData.getInterestRate();
                }
                loanTermVariations.add(loanTermVariation);
            }
        }

        return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency,
                loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType,
                nthDay, weekDayType, amortizationMethod, interestMethod, interestRatePerPeriod,
                interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
                allowPartialPeriodInterestCalcualtion, principalMoney, expectedDisbursementDate,
                repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment,
                graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsToleranceMoney,
                loanProduct.isMultiDisburseLoan(), emiAmount, disbursementDatas, maxOutstandingBalance,
                graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
                recalculationFrequencyType, restCalendarInstance, compoundingCalendarInstance,
                compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf,
                loanProduct.preCloseInterestCalculationStrategy(), calendar, BigDecimal.ZERO, loanTermVariations);
    }

    private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery,
            final PeriodFrequencyType repaymentPeriodFrequencyType, final LocalDate expectedDisbursementDate) {

        CalendarInstance restCalendarInstance = createInterestRecalculationCalendarInstance(
                expectedDisbursementDate, repaymentEvery, CalendarFrequencyType.from(repaymentPeriodFrequencyType));
        return restCalendarInstance;
    }

    private CalendarInstance createInterestRecalculationCalendarInstance(final LocalDate calendarStartDate,
            final RecalculationFrequencyType recalculationFrequencyType, final Integer frequency) {

        CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType.INVALID;
        switch (recalculationFrequencyType) {
        case DAILY:
            calendarFrequencyType = CalendarFrequencyType.DAILY;
            break;
        case MONTHLY:
            calendarFrequencyType = CalendarFrequencyType.MONTHLY;
            break;
        case WEEKLY:
            calendarFrequencyType = CalendarFrequencyType.WEEKLY;
            break;
        default:
            break;
        }

        return createInterestRecalculationCalendarInstance(calendarStartDate, frequency, calendarFrequencyType);
    }

    private CalendarInstance createInterestRecalculationCalendarInstance(final LocalDate calendarStartDate,
            final Integer frequency, CalendarFrequencyType calendarFrequencyType) {
        final Integer repeatsOnDay = calendarStartDate.getDayOfWeek();
        final String title = "loan_recalculation_detail";
        final Calendar calendar = Calendar.createRepeatingCalendar(title, calendarStartDate,
                CalendarType.COLLECTION.getValue(), calendarFrequencyType, frequency, repeatsOnDay);
        return CalendarInstance.from(calendar, null, CalendarEntityType.LOAN_RECALCULATION_REST_DETAIL.getValue());
    }

    private List<DisbursementData> fetchDisbursementData(final JsonObject command) {
        final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(command);
        final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(command);
        List<DisbursementData> disbursementDatas = new ArrayList<>();
        if (command.has(LoanApiConstants.disbursementDataParameterName)) {
            final JsonArray disbursementDataArray = command
                    .getAsJsonArray(LoanApiConstants.disbursementDataParameterName);
            if (disbursementDataArray != null && disbursementDataArray.size() > 0) {
                int i = 0;
                do {
                    final JsonObject jsonObject = disbursementDataArray.get(i).getAsJsonObject();
                    LocalDate expectedDisbursementDate = null;
                    BigDecimal principal = null;

                    if (jsonObject.has(LoanApiConstants.disbursementDateParameterName)) {
                        expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed(
                                LoanApiConstants.disbursementDateParameterName, jsonObject, dateFormat, locale);
                    }
                    if (jsonObject.has(LoanApiConstants.disbursementPrincipalParameterName)
                            && jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).isJsonPrimitive()
                            && StringUtils.isNotBlank((jsonObject
                                    .get(LoanApiConstants.disbursementPrincipalParameterName).getAsString()))) {
                        principal = jsonObject
                                .getAsJsonPrimitive(LoanApiConstants.disbursementPrincipalParameterName)
                                .getAsBigDecimal();
                    }
                    disbursementDatas
                            .add(new DisbursementData(null, expectedDisbursementDate, null, principal, null, null));
                    i++;
                } while (i < disbursementDataArray.size());
            }
        }
        return disbursementDatas;
    }

    private void validateRepaymentsStartDateWithMeetingDates(final LocalDate repaymentsStartingFromDate,
            final Calendar calendar) {
        if (repaymentsStartingFromDate != null && !CalendarUtils.isValidRedurringDate(calendar.getRecurrence(),
                calendar.getStartDateLocalDate(), repaymentsStartingFromDate)) {
            final String errorMessage = "First repayment date '" + repaymentsStartingFromDate
                    + "' do not fall on a meeting date";
            throw new LoanApplicationDateException("first.repayment.date.do.not.match.meeting.date", errorMessage,
                    repaymentsStartingFromDate);
        }
    }

    public void validateDisbursementDateWithMeetingDates(final LocalDate expectedDisbursementDate,
            final Calendar calendar) {
        // disbursement date should fall on a meeting date
        if (!calendar.isValidRecurringDate(expectedDisbursementDate)) {
            final String errorMessage = "Expected disbursement date '" + expectedDisbursementDate
                    + "' do not fall on a meeting date";
            throw new LoanApplicationDateException("disbursement.date.do.not.match.meeting.date", errorMessage,
                    expectedDisbursementDate);
        }

    }

    private void validateRepaymentFrequencyIsSameAsMeetingFrequency(final Integer meetingFrequency,
            final Integer repaymentFrequency, final Integer meetingInterval, final Integer repaymentInterval) {
        // meeting with daily frequency should allow loan products with any
        // frequency.
        if (!PeriodFrequencyType.DAYS.getValue().equals(meetingFrequency)) {
            // repayment frequency must match with meeting frequency
            if (!meetingFrequency.equals(repaymentFrequency)) {
                throw new MeetingFrequencyMismatchException("loanapplication.repayment.frequency",
                        "Loan repayment frequency period must match that of meeting frequency period",
                        repaymentFrequency);
            } else if (meetingFrequency.equals(repaymentFrequency)) {
                // repayment frequency is same as meeting frequency repayment
                // interval should be same or multiple of meeting interval
                if (repaymentInterval % meetingInterval != 0) {
                    // throw exception: Loan product frequency/interval
                    throw new MeetingFrequencyMismatchException("loanapplication.repayment.interval",
                            "Loan repayment repaid every # must equal or multiple of meeting interval "
                                    + meetingInterval,
                            meetingInterval, repaymentInterval);
                }
            }
        }
    }

    public LoanProductRelatedDetail assembleLoanProductRelatedDetail(final JsonElement element) {
        final LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element);
        return loanApplicationTerms.toLoanProductRelatedDetail();
    }

    public LoanScheduleModel assembleLoanScheduleFrom(final JsonElement element) {
        // This method is getting called from calculate loan schedule.
        final LoanApplicationTerms loanApplicationTerms = assembleLoanTerms(element);
        // Get holiday details
        final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();

        final Long clientId = this.fromApiJsonHelper.extractLongNamed("clientId", element);
        final Long groupId = this.fromApiJsonHelper.extractLongNamed("groupId", element);

        Client client = null;
        Group group = null;
        Long officeId = null;
        if (clientId != null) {
            client = this.clientRepository.findOneWithNotFoundDetection(clientId);
            officeId = client.getOffice().getId();
        } else if (groupId != null) {
            group = this.groupRepository.findOneWithNotFoundDetection(groupId);
            officeId = group.getOffice().getId();
        }

        final LocalDate expectedDisbursementDate = this.fromApiJsonHelper
                .extractLocalDateNamed("expectedDisbursementDate", element);
        final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId,
                expectedDisbursementDate.toDate(), HolidayStatusType.ACTIVE.getValue());
        final WorkingDays workingDays = this.workingDaysRepository.findOne();

        validateDisbursementDateIsOnNonWorkingDay(loanApplicationTerms.getExpectedDisbursementDate(), workingDays);
        validateDisbursementDateIsOnHoliday(loanApplicationTerms.getExpectedDisbursementDate(), isHolidayEnabled,
                holidays);

        return assembleLoanScheduleFrom(loanApplicationTerms, isHolidayEnabled, holidays, workingDays, element,
                null);
    }

    public LoanScheduleModel assembleLoanScheduleFrom(final LoanApplicationTerms loanApplicationTerms,
            final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays,
            final JsonElement element, Set<LoanDisbursementDetails> disbursementDetails) {

        final Set<LoanCharge> loanCharges = this.loanChargeAssembler.fromParsedJson(element, disbursementDetails);

        final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory
                .create(loanApplicationTerms.getInterestMethod());

        final RoundingMode roundingMode = MoneyHelper.getRoundingMode();
        final MathContext mc = new MathContext(8, roundingMode);

        HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);

        return loanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, detailDTO);
    }

    public LoanScheduleModel assembleForInterestRecalculation(final LoanApplicationTerms loanApplicationTerms,
            final Long officeId, List<LoanTransaction> transactions, final Set<LoanCharge> loanCharges,
            final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
            final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments,
            final LocalDate rescheduleFrom) {
        final RoundingMode roundingMode = MoneyHelper.getRoundingMode();
        final MathContext mc = new MathContext(8, roundingMode);
        final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();

        final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId,
                loanApplicationTerms.getExpectedDisbursementDate().toDate(), HolidayStatusType.ACTIVE.getValue());
        final WorkingDays workingDays = this.workingDaysRepository.findOne();

        final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory
                .create(loanApplicationTerms.getInterestMethod());
        HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);
        return loanScheduleGenerator
                .rescheduleNextInstallments(mc, loanApplicationTerms, loanCharges, detailDTO, transactions,
                        loanRepaymentScheduleTransactionProcessor, repaymentScheduleInstallments, rescheduleFrom)
                .getLoanScheduleModel();
    }

    public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
            LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final Long officeId,
            List<LoanTransaction> loanTransactions,
            final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
            final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments) {
        final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory
                .create(loanApplicationTerms.getInterestMethod());
        final RoundingMode roundingMode = MoneyHelper.getRoundingMode();
        final MathContext mc = new MathContext(8, roundingMode);

        final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
        final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId,
                loanApplicationTerms.getExpectedDisbursementDate().toDate(), HolidayStatusType.ACTIVE.getValue());
        final WorkingDays workingDays = this.workingDaysRepository.findOne();
        HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);

        return loanScheduleGenerator.calculatePrepaymentAmount(currency, onDate, loanApplicationTerms, mc,
                loanCharges, holidayDetailDTO, loanTransactions, loanRepaymentScheduleTransactionProcessor,
                repaymentScheduleInstallments);

    }

    public void assempleVariableScheduleFrom(final Loan loan, final String json) {
        this.variableLoanScheduleFromApiJsonValidator.validateSchedule(json, loan);

        List<LoanTermVariations> variations = loan.getLoanTermVariations();
        List<LoanTermVariations> newVariations = new ArrayList<>();
        extractLoanTermVariations(loan, json, newVariations);

        final Map<LocalDate, LocalDate> adjustDueDateVariations = new HashMap<>();

        if (!variations.isEmpty()) {
            List<LoanTermVariations> retainVariations = adjustExistingVariations(variations, newVariations,
                    adjustDueDateVariations);
            newVariations = retainVariations;
        }
        variations.addAll(newVariations);
        Collections.sort(variations, new LoanTermVariationsComparator());

        /*
         * List<LoanTermVariationsData> loanTermVariationsDatas = new
         * ArrayList<>();
         * loanTermVariationsDatas.addAll(loanApplicationTerms.getLoanTermVariations
         * ().getExceptionData()); loanApplicationTerms =
         * LoanApplicationTerms.assembleFrom(loanApplicationTerms,
         * loanTermVariationsDatas);
         */

        // date validations
        List<LoanRepaymentScheduleInstallment> installments = loan.fetchRepaymentScheduleInstallments();
        Set<LocalDate> dueDates = new TreeSet<>();
        LocalDate graceApplicable = loan.getExpectedDisbursedOnLocalDate();
        Integer graceOnPrincipal = loan.getLoanProductRelatedDetail().graceOnPrincipalPayment();
        if (graceOnPrincipal == null) {
            graceOnPrincipal = 0;
        }
        LocalDate lastDate = loan.getExpectedDisbursedOnLocalDate();
        for (LoanRepaymentScheduleInstallment installment : installments) {
            dueDates.add(installment.getDueDate());
            if (lastDate.isBefore(installment.getDueDate())) {
                lastDate = installment.getDueDate();
            }
            if (graceOnPrincipal.equals(installment.getInstallmentNumber())) {
                graceApplicable = installment.getDueDate();
            }
        }
        Collection<LocalDate> keySet = adjustDueDateVariations.keySet();
        dueDates.addAll(keySet);
        for (final LocalDate date : keySet) {
            LocalDate removeDate = adjustDueDateVariations.get(date);
            if (removeDate != null) {
                dueDates.remove(removeDate);
            }
        }

        Set<LocalDate> actualDueDates = new TreeSet<>(dueDates);
        final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
        final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
                .resource("loan");
        List<LocalDate> overlappings = new ArrayList<>();
        for (LoanTermVariations termVariations : variations) {
            switch (termVariations.getTermType()) {
            case INSERT_INSTALLMENT:
                if (dueDates.contains(termVariations.fetchTermApplicaDate())) {
                    overlappings.add(termVariations.fetchTermApplicaDate());
                } else {
                    dueDates.add(termVariations.fetchTermApplicaDate());
                }
                if (!graceApplicable.isBefore(termVariations.fetchTermApplicaDate())) {
                    baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
                            "variable.schedule.insert.not.allowed.before.grace.period",
                            "Loan schedule insert request invalid");
                }
                if (termVariations.fetchTermApplicaDate().isAfter(lastDate)) {
                    baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
                            "variable.schedule.insert.not.allowed.after.last.period.date",
                            "Loan schedule insert request invalid");
                } else if (termVariations.fetchTermApplicaDate().isBefore(loan.getExpectedDisbursedOnLocalDate())) {
                    baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
                            "variable.schedule.insert.not.allowed.before.disbursement.date",
                            "Loan schedule insert request invalid");
                }
                break;
            case DELETE_INSTALLMENT:
                if (dueDates.contains(termVariations.fetchTermApplicaDate())) {
                    dueDates.remove(termVariations.fetchTermApplicaDate());
                } else {
                    baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
                            "variable.schedule.remove.date.invalid", "Loan schedule remove request invalid");
                }
                if (termVariations.fetchTermApplicaDate().isEqual(lastDate)) {
                    baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
                            "variable.schedule.delete.not.allowed.for.last.period.date",
                            "Loan schedule remove request invalid");
                }
                break;
            case DUE_DATE:
                if (dueDates.contains(termVariations.fetchTermApplicaDate())) {

                    if (overlappings.contains(termVariations.fetchTermApplicaDate())) {
                        overlappings.remove(termVariations.fetchTermApplicaDate());
                    } else {
                        dueDates.remove(termVariations.fetchTermApplicaDate());
                    }
                } else {
                    baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
                            "variable.schedule.modify.date.invalid",
                            "Loan schedule modify due date request invalid");
                }
                if (dueDates.contains(termVariations.fetchDateValue())) {
                    overlappings.add(termVariations.fetchDateValue());
                } else {
                    dueDates.add(termVariations.fetchDateValue());
                }
                if (termVariations.fetchDateValue().isBefore(loan.getExpectedDisbursedOnLocalDate())) {
                    baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
                            "variable.schedule.insert.not.allowed.before.disbursement.date",
                            "Loan schedule insert request invalid");
                }
                if (termVariations.fetchTermApplicaDate().isEqual(lastDate)) {
                    lastDate = termVariations.fetchDateValue();
                }
                break;
            case PRINCIPAL_AMOUNT:
            case EMI_AMOUNT:
                if (!graceApplicable.isBefore(termVariations.fetchTermApplicaDate())) {
                    baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
                            "variable.schedule.amount.update.not.allowed.before.grace.period",
                            "Loan schedule modify request invalid");
                }
                if (!dueDates.contains(termVariations.fetchTermApplicaDate())) {
                    baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
                            "variable.schedule.amount.update.from.date.invalid",
                            "Loan schedule modify request invalid");
                }
                if (termVariations.fetchTermApplicaDate().isEqual(lastDate)) {
                    baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
                            "variable.schedule.amount.update.not.allowed.for.last.period",
                            "Loan schedule modify request invalid");
                }
                break;

            default:
                break;

            }

        }
        if (!overlappings.isEmpty()) {
            baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
                    "variable.schedule.modify.date.can.not.be.due.date", overlappings);
        }
        LoanProductVariableInstallmentConfig installmentConfig = loan.loanProduct()
                .loanProductVariableInstallmentConfig();
        final CalendarInstance loanCalendarInstance = calendarInstanceRepository
                .findCalendarInstaneByEntityId(loan.getId(), CalendarEntityType.LOANS.getValue());
        Calendar loanCalendar = null;
        if (loanCalendarInstance != null) {
            loanCalendar = loanCalendarInstance.getCalendar();
        }
        final Integer minGap = installmentConfig.getMinimumGap();
        final Integer maxGap = installmentConfig.getMaximumGap();

        LocalDate previousDate = loan.getDisbursementDate();
        for (LocalDate duedate : dueDates) {
            int gap = Days.daysBetween(previousDate, duedate).getDays();
            previousDate = duedate;
            if (gap < minGap || (maxGap != null && gap > maxGap)) {
                baseDataValidator.reset().value(duedate).failWithCodeNoParameterAddedToErrorCode(
                        "variable.schedule.date.must.be.in.min.max.range", "Loan schedule date invalid");
            } else if (loanCalendar != null && !actualDueDates.contains(duedate)
                    && !loanCalendar.isValidRecurringDate(duedate)) {
                baseDataValidator.reset().value(duedate).failWithCodeNoParameterAddedToErrorCode(
                        "variable.schedule.date.not.meeting.date",
                        "Loan schedule date not in sync with meeting date");
            }
        }
        if (!dataValidationErrors.isEmpty()) {
            throw new PlatformApiDataValidationException(dataValidationErrors);
        }
        if (loan.getExpectedFirstRepaymentOnDate() == null) {
            loan.setExpectedFirstRepaymentOnDate(loan.fetchRepaymentScheduleInstallment(1).getDueDate().toDate());
        }
        final LocalDate recalculateFrom = null;
        ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan,
                recalculateFrom);
        AppUser currentUser = this.context.getAuthenticatedUserIfPresent();
        loan.regenerateRepaymentSchedule(scheduleGeneratorDTO, currentUser);
    }

    private List<LoanTermVariations> adjustExistingVariations(List<LoanTermVariations> variations,
            List<LoanTermVariations> newVariations, final Map<LocalDate, LocalDate> adjustDueDateVariations) {
        Map<LocalDate, LoanTermVariations> amountVariations = new HashMap<>();
        Map<LocalDate, LoanTermVariations> dueDateVariations = new HashMap<>();
        Map<LocalDate, LoanTermVariations> insertVariations = new HashMap<>();
        for (LoanTermVariations loanTermVariations : variations) {
            switch (loanTermVariations.getTermType()) {
            case EMI_AMOUNT:
            case PRINCIPAL_AMOUNT:
                amountVariations.put(loanTermVariations.fetchTermApplicaDate(), loanTermVariations);
                break;
            case DUE_DATE:
                dueDateVariations.put(loanTermVariations.fetchDateValue(), loanTermVariations);
                adjustDueDateVariations.put(loanTermVariations.fetchTermApplicaDate(),
                        loanTermVariations.fetchDateValue());
                break;
            case INSERT_INSTALLMENT:
                insertVariations.put(loanTermVariations.fetchTermApplicaDate(), loanTermVariations);
                adjustDueDateVariations.put(loanTermVariations.fetchTermApplicaDate(),
                        loanTermVariations.fetchTermApplicaDate());
                break;
            case DELETE_INSTALLMENT:
                adjustDueDateVariations.put(loanTermVariations.fetchTermApplicaDate(), null);
                break;
            default:
                break;
            }
        }
        List<LoanTermVariations> retainVariations = new ArrayList<>();
        for (LoanTermVariations loanTermVariations : newVariations) {
            boolean retain = true;
            switch (loanTermVariations.getTermType()) {
            case DUE_DATE:
                if (amountVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) {
                    amountVariations.get(loanTermVariations.fetchTermApplicaDate())
                            .setTermApplicableFrom(loanTermVariations.getDateValue());
                } else if (insertVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) {
                    insertVariations.get(loanTermVariations.fetchTermApplicaDate())
                            .setTermApplicableFrom(loanTermVariations.getDateValue());
                    retain = false;
                }
                if (dueDateVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) {
                    LoanTermVariations existingVariation = dueDateVariations
                            .get(loanTermVariations.fetchTermApplicaDate());
                    if (existingVariation.fetchTermApplicaDate().isEqual(loanTermVariations.fetchDateValue())) {
                        variations.remove(existingVariation);
                    } else {
                        existingVariation.setTermApplicableFrom(loanTermVariations.getDateValue());
                    }
                    retain = false;
                }
                break;
            case EMI_AMOUNT:
            case PRINCIPAL_AMOUNT:
                if (amountVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) {
                    amountVariations.get(loanTermVariations.fetchTermApplicaDate())
                            .setDecimalValue(loanTermVariations.getTermValue());
                    retain = false;
                } else if (insertVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) {
                    insertVariations.get(loanTermVariations.fetchTermApplicaDate())
                            .setDecimalValue(loanTermVariations.getTermValue());
                    retain = false;
                }
                break;
            case DELETE_INSTALLMENT:
                if (amountVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) {
                    variations.remove(amountVariations.get(loanTermVariations.fetchTermApplicaDate()));

                } else if (insertVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) {
                    variations.remove(insertVariations.get(loanTermVariations.fetchTermApplicaDate()));
                    retain = false;
                }
                if (dueDateVariations.containsKey(loanTermVariations.fetchTermApplicaDate())) {
                    variations.remove(amountVariations.get(loanTermVariations.fetchTermApplicaDate()));
                }
                break;
            default:
                break;
            }
            if (retain) {
                retainVariations.add(loanTermVariations);
            }
        }
        return retainVariations;
    }

    private void extractLoanTermVariations(final Loan loan, final String json,
            final List<LoanTermVariations> loanTermVariations) {
        final JsonElement element = this.fromApiJsonHelper.parse(json);
        if (loan.loanProduct().allowVariabeInstallments()) {
            if (element.isJsonObject()
                    && this.fromApiJsonHelper.parameterExists(LoanApiConstants.exceptionParamName, element)) {
                final JsonObject topLevelJsonElement = element.getAsJsonObject();
                final String dateFormat = this.fromApiJsonHelper.extractDateFormatParameter(topLevelJsonElement);
                final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement);
                final JsonObject exceptionObject = topLevelJsonElement
                        .getAsJsonObject(LoanApiConstants.exceptionParamName);
                if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.modifiedinstallmentsParamName,
                        exceptionObject)
                        && exceptionObject.get(LoanApiConstants.modifiedinstallmentsParamName).isJsonArray()) {
                    final JsonArray modificationsArray = exceptionObject
                            .get(LoanApiConstants.modifiedinstallmentsParamName).getAsJsonArray();
                    extractLoanTermVariations(loan, dateFormat, locale, modificationsArray, false, false,
                            loanTermVariations);
                }
                if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.newinstallmentsParamName,
                        exceptionObject)
                        && exceptionObject.get(LoanApiConstants.newinstallmentsParamName).isJsonArray()) {
                    final JsonArray array = exceptionObject.get(LoanApiConstants.newinstallmentsParamName)
                            .getAsJsonArray();
                    extractLoanTermVariations(loan, dateFormat, locale, array, true, false, loanTermVariations);
                }
                if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.deletedinstallmentsParamName,
                        exceptionObject)
                        && exceptionObject.get(LoanApiConstants.deletedinstallmentsParamName).isJsonArray()) {
                    final JsonArray array = exceptionObject.get(LoanApiConstants.deletedinstallmentsParamName)
                            .getAsJsonArray();
                    extractLoanTermVariations(loan, dateFormat, locale, array, false, true, loanTermVariations);
                }
            }
        }
    }

    private void extractLoanTermVariations(final Loan loan, final String dateFormat, final Locale locale,
            final JsonArray modificationsArray, final boolean isInsertInstallment,
            final boolean isDeleteInstallment, final List<LoanTermVariations> loanTermVariations) {
        for (int i = 1; i <= modificationsArray.size(); i++) {
            final JsonObject arrayElement = modificationsArray.get(i - 1).getAsJsonObject();
            BigDecimal decimalValue = null;
            LoanTermVariationType decimalValueVariationType = LoanTermVariationType.INVALID;
            if (loan.getLoanProductRelatedDetail().getAmortizationMethod().isEqualInstallment()
                    && loan.getLoanProductRelatedDetail().getInterestMethod().isDecliningBalnce()) {
                decimalValue = this.fromApiJsonHelper
                        .extractBigDecimalNamed(LoanApiConstants.installmentAmountParamName, arrayElement, locale);
                decimalValueVariationType = LoanTermVariationType.EMI_AMOUNT;
            } else {
                decimalValue = this.fromApiJsonHelper.extractBigDecimalNamed(LoanApiConstants.principalParamName,
                        arrayElement, locale);
                decimalValueVariationType = LoanTermVariationType.PRINCIPAL_AMOUNT;
            }

            LocalDate duedateLocalDate = this.fromApiJsonHelper
                    .extractLocalDateNamed(LoanApiConstants.dueDateParamName, arrayElement, dateFormat, locale);
            Date dueDate = duedateLocalDate.toDate();

            LocalDate modifiedDuedateLocalDate = this.fromApiJsonHelper.extractLocalDateNamed(
                    LoanApiConstants.modifiedDueDateParamName, arrayElement, dateFormat, locale);
            Date modifiedDuedate = null;
            if (modifiedDuedateLocalDate != null) {
                modifiedDuedate = modifiedDuedateLocalDate.toDate();
            }
            boolean isSpecificToInstallment = true;
            if (isInsertInstallment) {
                LoanTermVariations data = new LoanTermVariations(
                        LoanTermVariationType.INSERT_INSTALLMENT.getValue(), dueDate, decimalValue, modifiedDuedate,
                        isSpecificToInstallment, loan);
                loanTermVariations.add(data);
            } else if (isDeleteInstallment) {
                LoanTermVariations data = new LoanTermVariations(
                        LoanTermVariationType.DELETE_INSTALLMENT.getValue(), dueDate, decimalValue, modifiedDuedate,
                        isSpecificToInstallment, loan);
                loanTermVariations.add(data);
            } else {
                if (modifiedDuedate != null) {
                    BigDecimal amountData = null;
                    LoanTermVariations data = new LoanTermVariations(LoanTermVariationType.DUE_DATE.getValue(),
                            dueDate, amountData, modifiedDuedate, isSpecificToInstallment, loan);
                    loanTermVariations.add(data);
                }
                if (decimalValue != null) {
                    if (modifiedDuedate == null) {
                        modifiedDuedate = dueDate;
                    }
                    Date date = null;
                    LoanTermVariations data = new LoanTermVariations(decimalValueVariationType.getValue(),
                            modifiedDuedate, decimalValue, date, isSpecificToInstallment, loan);
                    loanTermVariations.add(data);
                }
            }

        }
    }

    private void validateDisbursementDateIsOnNonWorkingDay(final LocalDate disbursementDate,
            final WorkingDays workingDays) {
        if (!WorkingDaysUtil.isWorkingDay(workingDays, disbursementDate)) {
            final String errorMessage = "The expected disbursement date cannot be on a non working day";
            throw new LoanApplicationDateException("disbursement.date.on.non.working.day", errorMessage,
                    disbursementDate);
        }
    }

    private void validateDisbursementDateIsOnHoliday(final LocalDate disbursementDate,
            final boolean isHolidayEnabled, final List<Holiday> holidays) {
        if (isHolidayEnabled) {
            if (HolidayUtil.isHoliday(disbursementDate, holidays)) {
                final String errorMessage = "The expected disbursement date cannot be on a holiday";
                throw new LoanApplicationDateException("disbursement.date.on.holiday", errorMessage,
                        disbursementDate);
            }
        }
    }

    private LocalDate deriveFirstRepaymentDate(final AccountType loanType, final Integer repaymentEvery,
            final LocalDate expectedDisbursementDate, final PeriodFrequencyType repaymentPeriodFrequencyType,
            final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final Calendar calendar) {

        LocalDate derivedFirstRepayment = null;

        final LocalDate dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment = expectedDisbursementDate
                .plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);

        if ((loanType.isJLGAccount() || loanType.isGroupAccount()) && calendar != null) {

            final LocalDate refernceDateForCalculatingFirstRepaymentDate = expectedDisbursementDate;
            derivedFirstRepayment = deriveFirstRepaymentDateForJLGLoans(repaymentEvery, expectedDisbursementDate,
                    refernceDateForCalculatingFirstRepaymentDate, repaymentPeriodFrequencyType,
                    minimumDaysBetweenDisbursalAndFirstRepayment, calendar);

        } /*** Individual or group account, or JLG not linked to a meeting ***/
        else {
            LocalDate dateBasedOnRepaymentFrequency;
            // Derive the first repayment date as greater date among
            // (disbursement date + plus frequency) or
            // (disbursement date + minimum between disbursal and first
            // repayment )
            if (repaymentPeriodFrequencyType.isDaily()) {
                dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusDays(repaymentEvery);
            } else if (repaymentPeriodFrequencyType.isWeekly()) {
                dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusWeeks(repaymentEvery);
            } else if (repaymentPeriodFrequencyType.isMonthly()) {
                dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusMonths(repaymentEvery);
            } /** yearly loan **/
            else {
                dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusYears(repaymentEvery);
            }
            derivedFirstRepayment = dateBasedOnRepaymentFrequency.isAfter(
                    dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment) ? dateBasedOnRepaymentFrequency
                            : dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment;
        }

        return derivedFirstRepayment;
    }

    private LocalDate deriveFirstRepaymentDateForJLGLoans(final Integer repaymentEvery,
            final LocalDate expectedDisbursementDate, final LocalDate refernceDateForCalculatingFirstRepaymentDate,
            final PeriodFrequencyType repaymentPeriodFrequencyType,
            final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final Calendar calendar) {

        final String frequency = CalendarUtils
                .getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
        final LocalDate derivedFirstRepayment = CalendarUtils.getFirstRepaymentMeetingDate(calendar,
                refernceDateForCalculatingFirstRepaymentDate, repaymentEvery, frequency);
        final LocalDate minimumFirstRepaymentDate = expectedDisbursementDate
                .plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
        return minimumFirstRepaymentDate.isBefore(derivedFirstRepayment) ? derivedFirstRepayment
                : deriveFirstRepaymentDateForJLGLoans(repaymentEvery, expectedDisbursementDate,
                        derivedFirstRepayment, repaymentPeriodFrequencyType,
                        minimumDaysBetweenDisbursalAndFirstRepayment, calendar);
    }

    private void validateMinimumDaysBetweenDisbursalAndFirstRepayment(final LocalDate disbursalDate,
            final LocalDate firstRepaymentDate, final Integer minimumDaysBetweenDisbursalAndFirstRepayment) {

        final LocalDate minimumFirstRepaymentDate = disbursalDate
                .plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
        if (firstRepaymentDate.isBefore(minimumFirstRepaymentDate)) {
            throw new MinDaysBetweenDisbursalAndFirstRepaymentViolationException(disbursalDate, firstRepaymentDate,
                    minimumDaysBetweenDisbursalAndFirstRepayment);
        }
    }
}