blob: 3fe845ea98c4639b611360ea434c4fcc8c6dfe7d [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.domain;
import com.google.common.base.Splitter;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import javax.persistence.Version;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.service.HolidayUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
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.office.domain.Office;
import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil;
import org.apache.fineract.portfolio.accountdetails.domain.AccountType;
import org.apache.fineract.portfolio.calendar.data.CalendarHistoryDataWrapper;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.calendar.domain.CalendarHistory;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
import org.apache.fineract.portfolio.calendar.domain.CalendarWeekDaysType;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.collateral.domain.LoanCollateral;
import org.apache.fineract.portfolio.common.domain.DayOfWeekType;
import org.apache.fineract.portfolio.common.domain.NthDayType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.creditscorecard.domain.CreditScorecard;
import org.apache.fineract.portfolio.floatingrates.data.FloatingRateDTO;
import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData;
import org.apache.fineract.portfolio.fund.domain.Fund;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.command.LoanChargeCommand;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException;
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException;
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
import org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbursementException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
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.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
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.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.rate.domain.Rate;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
import org.apache.fineract.useradministration.domain.AppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Entity
@Component
@Table(name = "m_loan", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "loan_account_no_UNIQUE"),
@UniqueConstraint(columnNames = { "external_id" }, name = "loan_externalid_UNIQUE") })
public class Loan extends AbstractPersistableCustom {
private static final Logger LOG = LoggerFactory.getLogger(Loan.class);
/** Disable optimistic locking till batch jobs failures can be fixed **/
@Version
int version;
@Column(name = "account_no", length = 20, unique = true, nullable = false)
private String accountNumber;
@Column(name = "external_id")
private String externalId;
@ManyToOne
@JoinColumn(name = "client_id", nullable = true)
private Client client;
@ManyToOne
@JoinColumn(name = "group_id", nullable = true)
private Group group;
@ManyToOne
@JoinColumn(name = "glim_id", nullable = true)
private GroupLoanIndividualMonitoringAccount glim;
@Column(name = "loan_type_enum", nullable = false)
private Integer loanType;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private LoanProduct loanProduct;
@ManyToOne(optional = true, fetch = FetchType.EAGER)
@JoinColumn(name = "fund_id", nullable = true)
private Fund fund;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "loan_officer_id", nullable = true)
private Staff loanOfficer;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "loanpurpose_cv_id", nullable = true)
private CodeValue loanPurpose;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "loan_transaction_strategy_id", nullable = true)
private LoanTransactionProcessingStrategy transactionProcessingStrategy;
@Embedded
private LoanProductRelatedDetail loanRepaymentScheduleDetail;
@Column(name = "term_frequency", nullable = false)
private Integer termFrequency;
@Column(name = "term_period_frequency_enum", nullable = false)
private Integer termPeriodFrequencyType;
@Column(name = "loan_status_id", nullable = false)
private Integer loanStatus;
@Column(name = "sync_disbursement_with_meeting", nullable = true)
private Boolean syncDisbursementWithMeeting;
// loan application states
@Temporal(TemporalType.DATE)
@Column(name = "submittedon_date")
private Date submittedOnDate;
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "submittedon_userid", nullable = true)
private AppUser submittedBy;
@Temporal(TemporalType.DATE)
@Column(name = "rejectedon_date")
private Date rejectedOnDate;
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "rejectedon_userid", nullable = true)
private AppUser rejectedBy;
@Temporal(TemporalType.DATE)
@Column(name = "withdrawnon_date")
private Date withdrawnOnDate;
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "withdrawnon_userid", nullable = true)
private AppUser withdrawnBy;
@Temporal(TemporalType.DATE)
@Column(name = "approvedon_date")
private Date approvedOnDate;
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "approvedon_userid", nullable = true)
private AppUser approvedBy;
@Temporal(TemporalType.DATE)
@Column(name = "expected_disbursedon_date")
private Date expectedDisbursementDate;
@Temporal(TemporalType.DATE)
@Column(name = "disbursedon_date")
private Date actualDisbursementDate;
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "disbursedon_userid", nullable = true)
private AppUser disbursedBy;
@Temporal(TemporalType.DATE)
@Column(name = "closedon_date")
private Date closedOnDate;
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "closedon_userid", nullable = true)
private AppUser closedBy;
@Temporal(TemporalType.DATE)
@Column(name = "writtenoffon_date")
private Date writtenOffOnDate;
@Temporal(TemporalType.DATE)
@Column(name = "rescheduledon_date")
private Date rescheduledOnDate;
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@JoinColumn(name = "rescheduledon_userid", nullable = true)
private AppUser rescheduledByUser;
@Temporal(TemporalType.DATE)
@Column(name = "expected_maturedon_date")
private Date expectedMaturityDate;
@Temporal(TemporalType.DATE)
@Column(name = "maturedon_date")
private Date actualMaturityDate;
@Temporal(TemporalType.DATE)
@Column(name = "expected_firstrepaymenton_date")
private Date expectedFirstRepaymentOnDate;
@Temporal(TemporalType.DATE)
@Column(name = "interest_calculated_from_date")
private Date interestChargedFromDate;
@Column(name = "total_overpaid_derived", scale = 6, precision = 19)
private BigDecimal totalOverpaid;
@Column(name = "loan_counter")
private Integer loanCounter;
@Column(name = "loan_product_counter")
private Integer loanProductCounter;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private Set<LoanCharge> charges = new HashSet<>();
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private Set<LoanTrancheCharge> trancheCharges = new HashSet<>();
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private Set<LoanCollateral> collateral = null;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private Set<LoanCollateralManagement> loanCollateralManagements = new HashSet<>();
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private Set<LoanOfficerAssignmentHistory> loanOfficerHistory;
@OrderBy(value = "installmentNumber")
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = new ArrayList<>();
@OrderBy(value = "dateOf, id")
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private List<LoanTransaction> loanTransactions = new ArrayList<>();
@Embedded
private LoanSummary summary;
@Transient
private boolean accountNumberRequiresAutoGeneration = false;
@Transient
private LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
@Transient
private LoanLifecycleStateMachine loanLifecycleStateMachine;
@Transient
private LoanSummaryWrapper loanSummaryWrapper;
@Column(name = "principal_amount_proposed", scale = 6, precision = 19, nullable = false)
private BigDecimal proposedPrincipal;
@Column(name = "approved_principal", scale = 6, precision = 19, nullable = false)
private BigDecimal approvedPrincipal;
@Column(name = "net_disbursal_amount", scale = 6, precision = 19, nullable = false)
private BigDecimal netDisbursalAmount;
@Column(name = "fixed_emi_amount", scale = 6, precision = 19, nullable = true)
private BigDecimal fixedEmiAmount;
@Column(name = "max_outstanding_loan_balance", scale = 6, precision = 19, nullable = true)
private BigDecimal maxOutstandingLoanBalance;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
@OrderBy(value = "expectedDisbursementDate, id")
private List<LoanDisbursementDetails> disbursementDetails = new ArrayList<>();
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private List<PostDatedChecks> postDatedChecks = new ArrayList<>();
@OrderBy(value = "termApplicableFrom, id")
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private List<LoanTermVariations> loanTermVariations = new ArrayList<>();
@Column(name = "total_recovered_derived", scale = 6, precision = 19)
private BigDecimal totalRecovered;
@OneToOne(cascade = CascadeType.ALL, mappedBy = "loan", optional = true, orphanRemoval = true, fetch = FetchType.LAZY)
private LoanInterestRecalculationDetails loanInterestRecalculationDetails;
@Column(name = "is_npa", nullable = false)
private boolean isNpa;
@Temporal(TemporalType.DATE)
@Column(name = "accrued_till")
private Date accruedTill;
@Column(name = "create_standing_instruction_at_disbursement", nullable = true)
private Boolean createStandingInstructionAtDisbursement;
@Column(name = "guarantee_amount_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal guaranteeAmountDerived;
@Temporal(TemporalType.DATE)
@Column(name = "interest_recalcualated_on")
private Date interestRecalculatedOn;
@Column(name = "is_floating_interest_rate", nullable = true)
private Boolean isFloatingInterestRate;
@Column(name = "interest_rate_differential", scale = 6, precision = 19, nullable = true)
private BigDecimal interestRateDifferential;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "writeoff_reason_cv_id", nullable = true)
private CodeValue writeOffReason;
@Column(name = "loan_sub_status_id", nullable = true)
private Integer loanSubStatus;
@Column(name = "is_topup", nullable = false)
private boolean isTopup = false;
@OneToOne(cascade = CascadeType.ALL, mappedBy = "loan", optional = true, orphanRemoval = true, fetch = FetchType.LAZY)
private LoanTopupDetails loanTopupDetails;
@OneToMany(fetch = FetchType.LAZY)
@JoinTable(name = "m_loan_rate", joinColumns = @JoinColumn(name = "loan_id"), inverseJoinColumns = @JoinColumn(name = "rate_id"))
private List<Rate> rates;
@Column(name = "fixed_principal_percentage_per_installment", scale = 2, precision = 5, nullable = true)
private BigDecimal fixedPrincipalPercentagePerInstallment;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "loan_credit_scorecard_id", referencedColumnName = "id")
private CreditScorecard scorecard;
public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType,
final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final LoanTransactionProcessingStrategy transactionProcessingStrategy,
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges,
final Set<LoanCollateralManagement> collateral, final BigDecimal fixedEmiAmount,
final List<LoanDisbursementDetails> disbursementDetails, final BigDecimal maxOutstandingLoanBalance,
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final CreditScorecard scorecard) {
final LoanStatus status = null;
final Group group = null;
final Boolean syncDisbursementWithMeeting = null;
return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting, fixedEmiAmount,
disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate,
interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment, scorecard);
}
public static Loan newGroupLoanApplication(final String accountNo, final Group group, final Integer loanType,
final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final LoanTransactionProcessingStrategy transactionProcessingStrategy,
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges,
final Set<LoanCollateralManagement> collateral, final Boolean syncDisbursementWithMeeting, final BigDecimal fixedEmiAmount,
final List<LoanDisbursementDetails> disbursementDetails, final BigDecimal maxOutstandingLoanBalance,
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final CreditScorecard scorecard) {
final LoanStatus status = null;
final Client client = null;
return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting, fixedEmiAmount,
disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate,
interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment, scorecard);
}
public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group,
final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final LoanTransactionProcessingStrategy transactionProcessingStrategy,
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges,
final Set<LoanCollateralManagement> collateral, final Boolean syncDisbursementWithMeeting, final BigDecimal fixedEmiAmount,
final List<LoanDisbursementDetails> disbursementDetails, final BigDecimal maxOutstandingLoanBalance,
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final CreditScorecard scorecard) {
final LoanStatus status = null;
return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, status, loanCharges, collateral, syncDisbursementWithMeeting, fixedEmiAmount,
disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate,
interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment, scorecard);
}
protected Loan() {
this.client = null;
}
private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund,
final Staff loanOfficer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy,
final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus,
final Set<LoanCharge> loanCharges, final Set<LoanCollateralManagement> collateral, final Boolean syncDisbursementWithMeeting,
final BigDecimal fixedEmiAmount, final List<LoanDisbursementDetails> disbursementDetails,
final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement,
final Boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final List<Rate> rates,
final BigDecimal fixedPrincipalPercentagePerInstallment, final CreditScorecard scorecard) {
this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail;
this.loanRepaymentScheduleDetail.validateRepaymentPeriodWithGraceSettings();
this.isFloatingInterestRate = isFloatingInterestRate;
this.interestRateDifferential = interestRateDifferential;
if (StringUtils.isBlank(accountNo)) {
this.accountNumber = new RandomPasswordGenerator(19).generate();
this.accountNumberRequiresAutoGeneration = true;
} else {
this.accountNumber = accountNo;
}
this.client = client;
this.group = group;
this.loanType = loanType;
this.fund = fund;
this.loanOfficer = loanOfficer;
this.loanPurpose = loanPurpose;
this.transactionProcessingStrategy = transactionProcessingStrategy;
this.loanProduct = loanProduct;
if (loanStatus != null) {
this.loanStatus = loanStatus.getValue();
} else {
this.loanStatus = null;
}
if (loanCharges != null && !loanCharges.isEmpty()) {
this.charges = associateChargesWithThisLoan(loanCharges);
this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
} else {
this.charges = null;
this.summary = new LoanSummary();
}
if (loanType.equals(1) && collateral != null && !collateral.isEmpty()) {
this.loanCollateralManagements = associateWithThisLoan(collateral);
} else {
this.loanCollateralManagements = null;
}
this.loanOfficerHistory = null;
this.syncDisbursementWithMeeting = syncDisbursementWithMeeting;
this.fixedEmiAmount = fixedEmiAmount;
this.maxOutstandingLoanBalance = maxOutstandingLoanBalance;
this.disbursementDetails = disbursementDetails;
this.approvedPrincipal = this.loanRepaymentScheduleDetail.getPrincipal().getAmount();
this.createStandingInstructionAtDisbursement = createStandingInstructionAtDisbursement;
/*
* During loan origination stage and before loan is approved principal_amount, approved_principal and
* principal_amount_demanded will same amount and that amount is same as applicant loan demanded amount.
*/
this.proposedPrincipal = this.loanRepaymentScheduleDetail.getPrincipal().getAmount();
// rates added here
this.rates = rates;
this.fixedPrincipalPercentagePerInstallment = fixedPrincipalPercentagePerInstallment;
// Add net get net disbursal amount from charges and principal
this.netDisbursalAmount = this.approvedPrincipal.subtract(deriveSumTotalOfChargesDueAtDisbursement());
this.scorecard = scorecard;
}
public Integer getNumberOfRepayments() {
return this.loanRepaymentScheduleDetail.getNumberOfRepayments();
}
private LoanSummary updateSummaryWithTotalFeeChargesDueAtDisbursement(final BigDecimal feeChargesDueAtDisbursement) {
if (this.summary == null) {
this.summary = LoanSummary.create(feeChargesDueAtDisbursement);
} else {
this.summary.updateTotalFeeChargesDueAtDisbursement(feeChargesDueAtDisbursement);
}
return this.summary;
}
public void updateLoanSummaryForUndoWaiveCharge(final BigDecimal amountWaived) {
this.summary.updateFeeChargesWaived(this.summary.getTotalFeeChargesWaived().subtract(amountWaived));
this.summary.updateFeeChargeOutstanding(this.summary.getTotalFeeChargesOutstanding().add(amountWaived));
}
private BigDecimal deriveSumTotalOfChargesDueAtDisbursement() {
Money chargesDue = Money.of(getCurrency(), BigDecimal.ZERO);
for (final LoanCharge charge : charges()) {
if (charge.isDueAtDisbursement()) {
chargesDue = chargesDue.plus(charge.amount());
}
}
return chargesDue.getAmount();
}
private Set<LoanCharge> associateChargesWithThisLoan(final Set<LoanCharge> loanCharges) {
for (final LoanCharge loanCharge : loanCharges) {
loanCharge.update(this);
if (loanCharge.getTrancheDisbursementCharge() != null) {
addTrancheLoanCharge(loanCharge.getCharge());
}
}
return loanCharges;
}
private Set<LoanCollateralManagement> associateWithThisLoan(final Set<LoanCollateralManagement> collateral) {
for (final LoanCollateralManagement item : collateral) {
item.setLoan(this);
}
return collateral;
}
public boolean isAccountNumberRequiresAutoGeneration() {
return this.accountNumberRequiresAutoGeneration;
}
public void setAccountNumberRequiresAutoGeneration(final boolean accountNumberRequiresAutoGeneration) {
this.accountNumberRequiresAutoGeneration = accountNumberRequiresAutoGeneration;
}
public void addLoanCharge(final LoanCharge loanCharge) {
validateLoanIsNotClosed(loanCharge);
if (isChargesAdditionAllowed() && loanCharge.isDueAtDisbursement()) {
// Note: added this constraint to restrict adding disbursement
// charges to a loan
// after it is disbursed
// if the loan charge payment type is 'Disbursement'.
// To undo this constraint would mean resolving how charges due are
// disbursement are handled at present.
// When a loan is disbursed and has charges due at disbursement, a
// transaction is created to auto record
// payment of the charges (user has no choice in saying they were or
// werent paid) - so its assumed they were paid.
final String defaultUserMessage = "This charge which is due at disbursement cannot be added as the loan is already disbursed.";
throw new LoanChargeCannotBeAddedException("loanCharge", "due.at.disbursement.and.loan.is.disbursed", defaultUserMessage,
getId(), loanCharge.name());
}
validateChargeHasValidSpecifiedDateIfApplicable(loanCharge, getDisbursementDate(), getLastRepaymentPeriodDueDate(false));
loanCharge.update(this);
final BigDecimal amount = calculateAmountPercentageAppliedTo(loanCharge);
BigDecimal chargeAmt = BigDecimal.ZERO;
BigDecimal totalChargeAmt = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageBased()) {
chargeAmt = loanCharge.getPercentage();
if (loanCharge.isInstalmentFee()) {
totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge);
} else if (loanCharge.isOverdueInstallmentCharge()) {
totalChargeAmt = loanCharge.amountOutstanding();
}
} else {
chargeAmt = loanCharge.amountOrPercentage();
}
loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt);
// NOTE: must add new loan charge to set of loan charges before
// reporcessing the repayment schedule.
if (this.charges == null) {
this.charges = new HashSet<>();
}
this.charges.add(loanCharge);
this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
// store Id's of existing loan transactions and existing reversed loan
// transactions
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), charges());
updateLoanSummaryDerivedFields();
}
public ChangedTransactionDetail reprocessTransactions() {
ChangedTransactionDetail changedTransactionDetail = null;
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), charges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
updateLoanSummaryDerivedFields();
this.loanTransactions.removeAll(changedTransactionDetail.getNewTransactionMappings().values());
return changedTransactionDetail;
}
/**
* Creates a loanTransaction for "Apply Charge Event" with transaction date set to "suppliedTransactionDate". The
* newly created transaction is also added to the Loan on which this method is called.
*
* If "suppliedTransactionDate" is not passed Id, the transaction date is set to the loans due date if the due date
* is lesser than todays date. If not, the transaction date is set to todays date
*
* @param loanCharge
* @param suppliedTransactionDate
* @return
*/
public LoanTransaction handleChargeAppliedTransaction(final LoanCharge loanCharge, final LocalDate suppliedTransactionDate,
final AppUser currentUser) {
final Money chargeAmount = loanCharge.getAmount(getCurrency());
Money feeCharges = chargeAmount;
Money penaltyCharges = Money.zero(loanCurrency());
if (loanCharge.isPenaltyCharge()) {
penaltyCharges = chargeAmount;
feeCharges = Money.zero(loanCurrency());
}
LocalDate transactionDate = null;
if (suppliedTransactionDate != null) {
transactionDate = suppliedTransactionDate;
} else {
transactionDate = loanCharge.getDueLocalDate();
final LocalDate currentDate = DateUtils.getLocalDateOfTenant();
// if loan charge is to be applied on a future date, the loan
// transaction would show todays date as applied date
if (transactionDate == null || currentDate.isBefore(transactionDate)) {
transactionDate = currentDate;
}
}
final LoanTransaction applyLoanChargeTransaction = LoanTransaction.accrueLoanCharge(this, getOffice(), chargeAmount,
transactionDate, feeCharges, penaltyCharges, DateUtils.getLocalDateTimeOfTenant(), currentUser);
Integer installmentNumber = null;
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(applyLoanChargeTransaction, loanCharge,
loanCharge.getAmount(getCurrency()).getAmount(), installmentNumber);
applyLoanChargeTransaction.getLoanChargesPaid().add(loanChargePaidBy);
addLoanTransaction(applyLoanChargeTransaction);
return applyLoanChargeTransaction;
}
private void handleChargePaidTransaction(final LoanCharge charge, final LoanTransaction chargesPayment,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final Integer installmentNumber) {
chargesPayment.updateLoan(this);
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge,
chargesPayment.getAmount(getCurrency()).getAmount(), installmentNumber);
chargesPayment.getLoanChargesPaid().add(loanChargePaidBy);
addLoanTransaction(chargesPayment);
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_PAYMENT,
LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final List<LoanRepaymentScheduleInstallment> chargePaymentInstallments = new ArrayList<>();
LocalDate startDate = getDisbursementDate();
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment installment : installments) {
if (installmentNumber == null && charge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate())) {
chargePaymentInstallments.add(installment);
break;
} else if (installmentNumber != null && installment.getInstallmentNumber().equals(installmentNumber)) {
chargePaymentInstallments.add(installment);
break;
}
startDate = installment.getDueDate();
}
final Set<LoanCharge> loanCharges = new HashSet<>(1);
loanCharges.add(charge);
loanRepaymentScheduleTransactionProcessor.handleTransaction(chargesPayment, getCurrency(), chargePaymentInstallments, loanCharges);
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(chargesPayment.getTransactionDate(), loanLifecycleStateMachine);
}
private void validateLoanIsNotClosed(final LoanCharge loanCharge) {
if (isClosed()) {
final String defaultUserMessage = "This charge cannot be added as the loan is already closed.";
throw new LoanChargeCannotBeAddedException("loanCharge", "loan.is.closed", defaultUserMessage, getId(), loanCharge.name());
}
}
private void validateLoanChargeIsNotWaived(final LoanCharge loanCharge) {
if (loanCharge.isWaived()) {
final String defaultUserMessage = "This loan charge cannot be removed as the charge as already been waived.";
throw new LoanChargeCannotBeAddedException("loanCharge", "loanCharge.is.waived", defaultUserMessage, getId(),
loanCharge.name());
}
}
private void validateChargeHasValidSpecifiedDateIfApplicable(final LoanCharge loanCharge, final LocalDate disbursementDate,
final LocalDate lastRepaymentPeriodDueDate) {
if (loanCharge.isSpecifiedDueDate()
&& !loanCharge.isDueForCollectionFromAndUpToAndIncluding(disbursementDate, lastRepaymentPeriodDueDate)) {
final String defaultUserMessage = "This charge with specified due date cannot be added as the it is not in schedule range.";
throw new LoanChargeCannotBeAddedException("loanCharge", "specified.due.date.outside.range", defaultUserMessage,
getDisbursementDate(), lastRepaymentPeriodDueDate, loanCharge.name());
}
}
private LocalDate getLastRepaymentPeriodDueDate(final boolean includeRecalculatedInterestComponent) {
LocalDate lastRepaymentDate = getDisbursementDate();
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (LoanRepaymentScheduleInstallment installment : installments) {
if ((includeRecalculatedInterestComponent || !installment.isRecalculatedInterestComponent())
&& installment.getDueDate().isAfter(lastRepaymentDate)) {
lastRepaymentDate = installment.getDueDate();
}
}
return lastRepaymentDate;
}
public void removeLoanCharge(final LoanCharge loanCharge) {
validateLoanIsNotClosed(loanCharge);
// NOTE: to remove this constraint requires that loan transactions
// that represent the waive of charges also be removed (or reversed)M
// if you want ability to remove loan charges that are waived.
validateLoanChargeIsNotWaived(loanCharge);
final boolean removed = loanCharge.isActive();
if (removed) {
loanCharge.setActive(false);
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), charges());
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
}
removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(loanCharge);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) {
/****
* TODO Vishwas Currently we do not allow removing a loan charge after a loan is approved (hence there is no
* need to adjust any loan transactions).
*
* Consider removing this block of code or logically completing it for the future by getting the list of
* affected Transactions
***/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement,
getCurrency(), getRepaymentScheduleInstallments(), charges());
}
this.charges.remove(loanCharge);
updateLoanSummaryDerivedFields();
}
private void removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(final LoanCharge loanCharge) {
if (loanCharge.isDueAtDisbursement()) {
LoanTransaction transactionToRemove = null;
List<LoanTransaction> transactions = getLoanTransactions();
for (final LoanTransaction transaction : transactions) {
if (transaction.isRepaymentAtDisbursement()
&& doesLoanChargePaidByContainLoanCharge(transaction.getLoanChargesPaid(), loanCharge)) {
final MonetaryCurrency currency = loanCurrency();
final Money chargeAmount = Money.of(currency, loanCharge.amount());
if (transaction.isGreaterThan(chargeAmount)) {
final Money principalPortion = Money.zero(currency);
final Money interestPortion = Money.zero(currency);
final Money penaltychargesPortion = Money.zero(currency);
transaction.updateComponentsAndTotal(principalPortion, interestPortion, chargeAmount, penaltychargesPortion);
} else {
transactionToRemove = transaction;
}
}
}
if (transactionToRemove != null) {
this.loanTransactions.remove(transactionToRemove);
}
}
}
private boolean doesLoanChargePaidByContainLoanCharge(Set<LoanChargePaidBy> loanChargePaidBys, LoanCharge loanCharge) {
for (LoanChargePaidBy loanChargePaidBy : loanChargePaidBys) {
if (loanChargePaidBy.getLoanCharge().equals(loanCharge)) {
return true;
}
}
return false;
}
public Map<String, Object> updateLoanCharge(final LoanCharge loanCharge, final JsonCommand command) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(3);
validateLoanIsNotClosed(loanCharge);
if (charges().contains(loanCharge)) {
final BigDecimal amount = calculateAmountPercentageAppliedTo(loanCharge);
final Map<String, Object> loanChargeChanges = loanCharge.update(command, amount);
actualChanges.putAll(loanChargeChanges);
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
if (!loanCharge.isDueAtDisbursement()) {
/****
* TODO Vishwas Currently we do not allow waiving updating loan charge after a loan is approved (hence there
* is no need to adjust any loan transactions).
*
* Consider removing this block of code or logically completing it for the future by getting the list of
* affected Transactions
***/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement,
getCurrency(), getRepaymentScheduleInstallments(), charges());
} else {
// reprocess loan schedule based on charge been waived.
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), charges());
}
updateLoanSummaryDerivedFields();
return actualChanges;
}
/**
* @param loanCharge
* @return
*/
private BigDecimal calculateAmountPercentageAppliedTo(final LoanCharge loanCharge) {
BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.isOverdueInstallmentCharge()) {
return loanCharge.getAmountPercentageAppliedTo();
}
switch (loanCharge.getChargeCalculation()) {
case PERCENT_OF_AMOUNT:
amount = getDerivedAmountForCharge(loanCharge);
break;
case PERCENT_OF_AMOUNT_AND_INTEREST:
final BigDecimal totalInterestCharged = getTotalInterest();
if (isMultiDisburmentLoan() && loanCharge.isDisbursementCharge()) {
amount = getTotalAllTrancheDisbursementAmount().getAmount().add(totalInterestCharged);
} else {
amount = getPrincpal().getAmount().add(totalInterestCharged);
}
break;
case PERCENT_OF_INTEREST:
amount = getTotalInterest();
break;
case PERCENT_OF_DISBURSEMENT_AMOUNT:
if (loanCharge.getTrancheDisbursementCharge() != null) {
amount = loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().principal();
} else {
amount = getPrincpal().getAmount();
}
break;
default:
break;
}
return amount;
}
private Money getTotalAllTrancheDisbursementAmount() {
Money amount = Money.zero(getCurrency());
if (isMultiDisburmentLoan()) {
for (final LoanDisbursementDetails loanDisbursementDetail : this.disbursementDetails) {
amount = amount.plus(loanDisbursementDetail.principal());
}
}
return amount;
}
/**
* @return
*/
public BigDecimal getTotalInterest() {
return this.loanSummaryWrapper.calculateTotalInterestCharged(getRepaymentScheduleInstallments(), getCurrency()).getAmount();
}
private BigDecimal calculatePerInstallmentChargeAmount(final LoanCharge loanCharge) {
return calculatePerInstallmentChargeAmount(loanCharge.getChargeCalculation(), loanCharge.getPercentage());
}
public BigDecimal calculatePerInstallmentChargeAmount(final ChargeCalculationType calculationType, final BigDecimal percentage) {
Money amount = Money.zero(getCurrency());
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment installment : installments) {
amount = amount.plus(calculateInstallmentChargeAmount(calculationType, percentage, installment));
}
return amount.getAmount();
}
public BigDecimal getTotalWrittenOff() {
return this.summary.getTotalWrittenOff();
}
/**
* @param calculationType
* @param percentage
* @param installment
* @return
*/
private Money calculateInstallmentChargeAmount(final ChargeCalculationType calculationType, final BigDecimal percentage,
final LoanRepaymentScheduleInstallment installment) {
Money amount = Money.zero(getCurrency());
Money percentOf = Money.zero(getCurrency());
switch (calculationType) {
case PERCENT_OF_AMOUNT:
percentOf = installment.getPrincipal(getCurrency());
break;
case PERCENT_OF_AMOUNT_AND_INTEREST:
percentOf = installment.getPrincipal(getCurrency()).plus(installment.getInterestCharged(getCurrency()));
break;
case PERCENT_OF_INTEREST:
percentOf = installment.getInterestCharged(getCurrency());
break;
default:
break;
}
amount = amount.plus(LoanCharge.percentageOf(percentOf.getAmount(), percentage));
return amount;
}
public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final Integer loanInstallmentNumber, final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge,
final AppUser currentUser) {
validateLoanIsNotClosed(loanCharge);
final Money amountWaived = loanCharge.waive(loanCurrency(), loanInstallmentNumber);
changes.put("amount", amountWaived.getAmount());
Money unrecognizedIncome = amountWaived.zero();
Money chargeComponent = amountWaived;
if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
Money receivableCharge = Money.zero(getCurrency());
if (loanInstallmentNumber != null) {
receivableCharge = accruedCharge
.minus(loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getAmountPaid(getCurrency()));
} else {
receivableCharge = accruedCharge.minus(loanCharge.getAmountPaid(getCurrency()));
}
if (receivableCharge.isLessThanZero()) {
receivableCharge = amountWaived.zero();
}
if (amountWaived.isGreaterThan(receivableCharge)) {
chargeComponent = receivableCharge;
unrecognizedIncome = amountWaived.minus(receivableCharge);
}
}
Money feeChargesWaived = chargeComponent;
Money penaltyChargesWaived = Money.zero(loanCurrency());
if (loanCharge.isPenaltyCharge()) {
penaltyChargesWaived = chargeComponent;
feeChargesWaived = Money.zero(loanCurrency());
}
LocalDate transactionDate = getDisbursementDate();
if (loanCharge.isDueDateCharge()) {
if (loanCharge.getDueLocalDate().isAfter(DateUtils.getLocalDateOfTenant())) {
transactionDate = DateUtils.getLocalDateOfTenant();
} else {
transactionDate = loanCharge.getDueLocalDate();
}
} else if (loanCharge.isInstalmentFee()) {
transactionDate = loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getRepaymentInstallment().getDueDate();
}
scheduleGeneratorDTO.setRecalculateFrom(transactionDate);
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final LoanTransaction waiveLoanChargeTransaction = LoanTransaction.waiveLoanCharge(this, getOffice(), amountWaived, transactionDate,
feeChargesWaived, penaltyChargesWaived, unrecognizedIncome, DateUtils.getLocalDateTimeOfTenant(), currentUser);
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(waiveLoanChargeTransaction, loanCharge,
waiveLoanChargeTransaction.getAmount(getCurrency()).getAmount(), loanInstallmentNumber);
waiveLoanChargeTransaction.getLoanChargesPaid().add(loanChargePaidBy);
addLoanTransaction(waiveLoanChargeTransaction);
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled() && (loanCharge.getDueLocalDate() == null
|| LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).isAfter(loanCharge.getDueLocalDate()))) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO, currentUser);
}
// Waive of charges whose due date falls after latest 'repayment'
// transaction dont require entire loan schedule to be reprocessed.
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) {
/****
* TODO Vishwas Currently we do not allow waiving fully paid loan charge and waiving partially paid loan
* charges only waives the remaining amount.
*
* Consider removing this block of code or logically completing it for the future by getting the list of
* affected Transactions
***/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement,
getCurrency(), getRepaymentScheduleInstallments(), charges());
} else {
// reprocess loan schedule based on charge been waived.
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), charges());
}
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(waiveLoanChargeTransaction.getTransactionDate(), loanLifecycleStateMachine);
return waiveLoanChargeTransaction;
}
public Client client() {
return this.client;
}
public GroupLoanIndividualMonitoringAccount getGlim() {
return glim;
}
public void setGlim(GroupLoanIndividualMonitoringAccount glim) {
this.glim = glim;
}
public LoanProduct loanProduct() {
return this.loanProduct;
}
public LoanProductRelatedDetail repaymentScheduleDetail() {
return this.loanRepaymentScheduleDetail;
}
public void updateClient(final Client client) {
this.client = client;
}
public void updateLoanProduct(final LoanProduct loanProduct) {
this.loanProduct = loanProduct;
}
public void updateAccountNo(final String newAccountNo) {
this.accountNumber = newAccountNo;
this.accountNumberRequiresAutoGeneration = false;
}
public void updateFund(final Fund fund) {
this.fund = fund;
}
public void updateLoanPurpose(final CodeValue loanPurpose) {
this.loanPurpose = loanPurpose;
}
public void updateLoanOfficerOnLoanApplication(final Staff newLoanOfficer) {
if (!isSubmittedAndPendingApproval()) {
Long loanOfficerId = null;
if (this.loanOfficer != null) {
loanOfficerId = this.loanOfficer.getId();
}
throw new LoanOfficerAssignmentException(getId(), loanOfficerId);
}
this.loanOfficer = newLoanOfficer;
}
public void updateTransactionProcessingStrategy(final LoanTransactionProcessingStrategy strategy) {
this.transactionProcessingStrategy = strategy;
}
public void updateLoanCharges(final Set<LoanCharge> loanCharges) {
List<Long> existingCharges = fetchAllLoanChargeIds();
/** Process new and updated charges **/
for (final LoanCharge loanCharge : loanCharges) {
LoanCharge charge = loanCharge;
// add new charges
if (loanCharge.getId() == null) {
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = null;
loanCharge.update(this);
if (this.loanProduct.isMultiDisburseLoan() && loanCharge.isTrancheDisbursementCharge()) {
loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().updateLoan(this);
for (final LoanDisbursementDetails loanDisbursementDetails : this.disbursementDetails) {
if (loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().getId() == null) {
if (loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().equals(loanDisbursementDetails)) {
loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, loanDisbursementDetails);
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
}
}
}
}
this.charges.add(loanCharge);
} else {
charge = fetchLoanChargesById(charge.getId());
if (charge != null) {
existingCharges.remove(charge.getId());
}
}
final BigDecimal amount = calculateAmountPercentageAppliedTo(loanCharge);
BigDecimal chargeAmt = BigDecimal.ZERO;
BigDecimal totalChargeAmt = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageBased()) {
chargeAmt = loanCharge.getPercentage();
if (loanCharge.isInstalmentFee()) {
totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge);
}
} else {
chargeAmt = loanCharge.amountOrPercentage();
}
if (charge != null) {
charge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt);
}
}
/** Updated deleted charges **/
for (Long id : existingCharges) {
fetchLoanChargesById(id).setActive(false);
}
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
}
public void updateLoanCollateral(final Set<LoanCollateralManagement> loanCollateral) {
if (this.loanCollateralManagements == null) {
this.loanCollateralManagements = new HashSet<>();
}
this.loanCollateralManagements.clear();
this.loanCollateralManagements.addAll(associateWithThisLoan(loanCollateral));
}
public void updateLoanRates(final List<Rate> loanRates) {
if (this.rates == null) {
this.rates = new ArrayList<>();
}
this.rates.clear();
this.rates.addAll(loanRates);
}
public void updateLoanSchedule(final LoanScheduleModel modifiedLoanSchedule, AppUser currentUser) {
this.repaymentScheduleInstallments.clear();
for (final LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) {
if (scheduledLoanInstallment.isRepaymentPeriod()) {
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(this,
scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent(),
scheduledLoanInstallment.getLoanCompoundingDetails());
addLoanRepaymentScheduleInstallment(installment);
}
}
updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields();
applyAccurals(currentUser);
}
public void updateLoanSchedule(final Collection<LoanRepaymentScheduleInstallment> installments, AppUser currentUser) {
List<LoanRepaymentScheduleInstallment> existingInstallments = new ArrayList<>(this.repaymentScheduleInstallments);
repaymentScheduleInstallments.clear();
for (final LoanRepaymentScheduleInstallment installment : installments) {
LoanRepaymentScheduleInstallment existingInstallment = findByInstallmentNumber(existingInstallments,
installment.getInstallmentNumber());
if (existingInstallment != null) {
Set<LoanInstallmentCharge> existingCharges = existingInstallment.getInstallmentCharges();
installment.getInstallmentCharges().addAll(existingCharges);
existingCharges.forEach(c -> c.setInstallment(installment));
existingInstallment.getInstallmentCharges().clear();
}
addLoanRepaymentScheduleInstallment(installment);
}
updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields();
applyAccurals(currentUser);
}
private LoanRepaymentScheduleInstallment findByInstallmentNumber(Collection<LoanRepaymentScheduleInstallment> installments,
Integer installmentNumber) {
for (LoanRepaymentScheduleInstallment installment : installments) {
if (Objects.equals(installment.getInstallmentNumber(), installmentNumber)) {
return installment;
}
}
return null;
}
/**
* method updates accrual derived fields on installments and reverse the unprocessed transactions
*/
private void applyAccurals(AppUser currentUser) {
Collection<LoanTransaction> accruals = retreiveListOfAccrualTransactions();
if (accruals.size() > 0) {
if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
applyPeriodicAccruals(accruals);
} else if (isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) {
updateAccrualsForNonPeriodicAccruals(accruals, currentUser);
}
}
}
private void applyPeriodicAccruals(final Collection<LoanTransaction> accruals) {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (LoanRepaymentScheduleInstallment installment : installments) {
Money interest = Money.zero(getCurrency());
Money fee = Money.zero(getCurrency());
Money penality = Money.zero(getCurrency());
for (LoanTransaction loanTransaction : accruals) {
if (loanTransaction.getTransactionDate().isAfter(installment.getFromDate())
&& !loanTransaction.getTransactionDate().isAfter(installment.getDueDate())) {
interest = interest.plus(loanTransaction.getInterestPortion(getCurrency()));
fee = fee.plus(loanTransaction.getFeeChargesPortion(getCurrency()));
penality = penality.plus(loanTransaction.getPenaltyChargesPortion(getCurrency()));
if (installment.getFeeChargesCharged(getCurrency()).isLessThan(fee)
|| installment.getInterestCharged(getCurrency()).isLessThan(interest)
|| installment.getPenaltyChargesCharged(getCurrency()).isLessThan(penality)
|| (getAccruedTill().isEqual(loanTransaction.getTransactionDate())
&& !installment.getDueDate().isEqual(getAccruedTill()))) {
interest = interest.minus(loanTransaction.getInterestPortion(getCurrency()));
fee = fee.minus(loanTransaction.getFeeChargesPortion(getCurrency()));
penality = penality.minus(loanTransaction.getPenaltyChargesPortion(getCurrency()));
loanTransaction.reverse();
}
}
}
installment.updateAccrualPortion(interest, fee, penality);
}
LoanRepaymentScheduleInstallment lastInstallment = getRepaymentScheduleInstallments()
.get(getRepaymentScheduleInstallments().size() - 1);
for (LoanTransaction loanTransaction : accruals) {
if (loanTransaction.getTransactionDate().isAfter(lastInstallment.getDueDate()) && !loanTransaction.isReversed()) {
loanTransaction.reverse();
}
}
}
private void updateAccrualsForNonPeriodicAccruals(final Collection<LoanTransaction> accruals, final AppUser currentUser) {
final Money interestApplied = Money.of(getCurrency(), this.summary.getTotalInterestCharged());
for (LoanTransaction loanTransaction : accruals) {
if (loanTransaction.getInterestPortion(getCurrency()).isGreaterThanZero()) {
if (loanTransaction.getInterestPortion(getCurrency()).isNotEqualTo(interestApplied)) {
loanTransaction.reverse();
final LocalDateTime currentDateTime = DateUtils.getLocalDateTimeOfTenant();
final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(getOffice(), this, interestApplied,
getDisbursementDate(), currentDateTime, currentUser);
addLoanTransaction(interestAppliedTransaction);
}
} else {
Set<LoanChargePaidBy> chargePaidBies = loanTransaction.getLoanChargesPaid();
for (final LoanChargePaidBy chargePaidBy : chargePaidBies) {
LoanCharge loanCharge = chargePaidBy.getLoanCharge();
Money chargeAmount = loanCharge.getAmount(getCurrency());
if (chargeAmount.isNotEqualTo(loanTransaction.getAmount(getCurrency()))) {
loanTransaction.reverse();
handleChargeAppliedTransaction(loanCharge, loanTransaction.getTransactionDate(), currentUser);
}
}
}
}
}
public void updateLoanScheduleDependentDerivedFields() {
if (this.getLoanRepaymentScheduleInstallmentsSize() > 0) {
this.expectedMaturityDate = Date.from(determineExpectedMaturityDate().atStartOfDay(ZoneId.systemDefault()).toInstant());
this.actualMaturityDate = Date.from(determineExpectedMaturityDate().atStartOfDay(ZoneId.systemDefault()).toInstant());
}
}
private void updateLoanSummaryDerivedFields() {
if (isNotDisbursed()) {
this.summary.zeroFields();
this.totalOverpaid = null;
} else {
final Money overpaidBy = calculateTotalOverpayment();
this.totalOverpaid = overpaidBy.getAmountDefaultedToNullIfZero();
final Money recoveredAmount = calculateTotalRecoveredPayments();
this.totalRecovered = recoveredAmount.getAmountDefaultedToNullIfZero();
final Money principal = this.loanRepaymentScheduleDetail.getPrincipal();
this.summary.updateSummary(loanCurrency(), principal, getRepaymentScheduleInstallments(), this.loanSummaryWrapper,
isDisbursed(), this.charges);
updateLoanOutstandingBalaces();
}
}
public void updateLoanSummarAndStatus() {
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(getLastUserTransactionDate(), loanLifecycleStateMachine);
}
public Map<String, Object> loanApplicationModification(final JsonCommand command, final Set<LoanCharge> possiblyModifedLoanCharges,
final Set<LoanCollateralManagement> possiblyModifedLoanCollateralItems, final AprCalculator aprCalculator,
boolean isChargesModified, final LoanProduct loanProduct) {
final Map<String, Object> actualChanges = this.loanRepaymentScheduleDetail.updateLoanApplicationAttributes(command, aprCalculator);
final MonetaryCurrency currency = new MonetaryCurrency(loanProduct.getCurrency().getCode(),
loanProduct.getCurrency().getDigitsAfterDecimal(), loanProduct.getCurrency().getCurrencyInMultiplesOf());
this.loanRepaymentScheduleDetail.updateCurrency(currency);
if (!actualChanges.isEmpty()) {
final boolean recalculateLoanSchedule = !(actualChanges.size() == 1 && actualChanges.containsKey("inArrearsTolerance"));
actualChanges.put("recalculateLoanSchedule", recalculateLoanSchedule);
isChargesModified = true;
}
final String dateFormatAsInput = command.dateFormat();
final String localeAsInput = command.locale();
final String accountNoParamName = "accountNo";
if (command.isChangeInStringParameterNamed(accountNoParamName, this.accountNumber)) {
final String newValue = command.stringValueOfParameterNamed(accountNoParamName);
actualChanges.put(accountNoParamName, newValue);
this.accountNumber = StringUtils.defaultIfEmpty(newValue, null);
}
final String createSiAtDisbursementParameterName = "createStandingInstructionAtDisbursement";
if (command.isChangeInBooleanParameterNamed(createSiAtDisbursementParameterName, shouldCreateStandingInstructionAtDisbursement())) {
final Boolean valueAsInput = command.booleanObjectValueOfParameterNamed(createSiAtDisbursementParameterName);
actualChanges.put(createSiAtDisbursementParameterName, valueAsInput);
this.createStandingInstructionAtDisbursement = valueAsInput;
}
final String externalIdParamName = "externalId";
if (command.isChangeInStringParameterNamed(externalIdParamName, this.externalId)) {
final String newValue = command.stringValueOfParameterNamed(externalIdParamName);
actualChanges.put(externalIdParamName, newValue);
this.externalId = StringUtils.defaultIfEmpty(newValue, null);
}
// add clientId, groupId and loanType changes to actual changes
final String clientIdParamName = "clientId";
final Long clientId = this.client == null ? null : this.client.getId();
if (command.isChangeInLongParameterNamed(clientIdParamName, clientId)) {
final Long newValue = command.longValueOfParameterNamed(clientIdParamName);
actualChanges.put(clientIdParamName, newValue);
}
// FIXME: AA - We may require separate api command to move loan from one
// group to another
final String groupIdParamName = "groupId";
final Long groupId = this.group == null ? null : this.group.getId();
if (command.isChangeInLongParameterNamed(groupIdParamName, groupId)) {
final Long newValue = command.longValueOfParameterNamed(groupIdParamName);
actualChanges.put(groupIdParamName, newValue);
}
final String productIdParamName = "productId";
if (command.isChangeInLongParameterNamed(productIdParamName, this.loanProduct.getId())) {
final Long newValue = command.longValueOfParameterNamed(productIdParamName);
actualChanges.put(productIdParamName, newValue);
actualChanges.put("recalculateLoanSchedule", true);
}
final String isFloatingInterestRateParamName = "isFloatingInterestRate";
if (command.isChangeInBooleanParameterNamed(isFloatingInterestRateParamName, this.isFloatingInterestRate)) {
final Boolean newValue = command.booleanObjectValueOfParameterNamed(isFloatingInterestRateParamName);
actualChanges.put(isFloatingInterestRateParamName, newValue);
this.isFloatingInterestRate = newValue;
}
final String interestRateDifferentialParamName = "interestRateDifferential";
if (command.isChangeInBigDecimalParameterNamed(interestRateDifferentialParamName, this.interestRateDifferential)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(interestRateDifferentialParamName);
actualChanges.put(interestRateDifferentialParamName, newValue);
this.interestRateDifferential = newValue;
}
Long existingFundId = null;
if (this.fund != null) {
existingFundId = this.fund.getId();
}
final String fundIdParamName = "fundId";
if (command.isChangeInLongParameterNamed(fundIdParamName, existingFundId)) {
final Long newValue = command.longValueOfParameterNamed(fundIdParamName);
actualChanges.put(fundIdParamName, newValue);
}
Long existingLoanOfficerId = null;
if (this.loanOfficer != null) {
existingLoanOfficerId = this.loanOfficer.getId();
}
final String loanOfficerIdParamName = "loanOfficerId";
if (command.isChangeInLongParameterNamed(loanOfficerIdParamName, existingLoanOfficerId)) {
final Long newValue = command.longValueOfParameterNamed(loanOfficerIdParamName);
actualChanges.put(loanOfficerIdParamName, newValue);
}
Long existingLoanPurposeId = null;
if (this.loanPurpose != null) {
existingLoanPurposeId = this.loanPurpose.getId();
}
final String loanPurposeIdParamName = "loanPurposeId";
if (command.isChangeInLongParameterNamed(loanPurposeIdParamName, existingLoanPurposeId)) {
final Long newValue = command.longValueOfParameterNamed(loanPurposeIdParamName);
actualChanges.put(loanPurposeIdParamName, newValue);
}
final String strategyIdParamName = "transactionProcessingStrategyId";
if (command.isChangeInLongParameterNamed(strategyIdParamName, this.transactionProcessingStrategy.getId())) {
final Long newValue = command.longValueOfParameterNamed(strategyIdParamName);
actualChanges.put(strategyIdParamName, newValue);
}
final String submittedOnDateParamName = "submittedOnDate";
if (command.isChangeInLocalDateParameterNamed(submittedOnDateParamName, getSubmittedOnDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(submittedOnDateParamName);
actualChanges.put(submittedOnDateParamName, valueAsInput);
actualChanges.put("dateFormat", dateFormatAsInput);
actualChanges.put("locale", localeAsInput);
final LocalDate newValue = command.localDateValueOfParameterNamed(submittedOnDateParamName);
this.submittedOnDate = Date.from(newValue.atStartOfDay(ZoneId.systemDefault()).toInstant());
}
final String expectedDisbursementDateParamName = "expectedDisbursementDate";
if (command.isChangeInLocalDateParameterNamed(expectedDisbursementDateParamName, getExpectedDisbursedOnLocalDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(expectedDisbursementDateParamName);
actualChanges.put(expectedDisbursementDateParamName, valueAsInput);
actualChanges.put("dateFormat", dateFormatAsInput);
actualChanges.put("locale", localeAsInput);
actualChanges.put("recalculateLoanSchedule", true);
final LocalDate newValue = command.localDateValueOfParameterNamed(expectedDisbursementDateParamName);
this.expectedDisbursementDate = Date.from(newValue.atStartOfDay(ZoneId.systemDefault()).toInstant());
removeFirstDisbursementTransaction();
}
final String repaymentsStartingFromDateParamName = "repaymentsStartingFromDate";
if (command.isChangeInLocalDateParameterNamed(repaymentsStartingFromDateParamName, getExpectedFirstRepaymentOnDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(repaymentsStartingFromDateParamName);
actualChanges.put(repaymentsStartingFromDateParamName, valueAsInput);
actualChanges.put("dateFormat", dateFormatAsInput);
actualChanges.put("locale", localeAsInput);
actualChanges.put("recalculateLoanSchedule", true);
final LocalDate newValue = command.localDateValueOfParameterNamed(repaymentsStartingFromDateParamName);
if (newValue != null) {
this.expectedFirstRepaymentOnDate = Date.from(newValue.atStartOfDay(ZoneId.systemDefault()).toInstant());
} else {
this.expectedFirstRepaymentOnDate = null;
}
}
final String syncDisbursementParameterName = "syncDisbursementWithMeeting";
if (command.isChangeInBooleanParameterNamed(syncDisbursementParameterName, isSyncDisbursementWithMeeting())) {
final Boolean valueAsInput = command.booleanObjectValueOfParameterNamed(syncDisbursementParameterName);
actualChanges.put(syncDisbursementParameterName, valueAsInput);
this.syncDisbursementWithMeeting = valueAsInput;
}
final String interestChargedFromDateParamName = "interestChargedFromDate";
if (command.isChangeInLocalDateParameterNamed(interestChargedFromDateParamName, getInterestChargedFromDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(interestChargedFromDateParamName);
actualChanges.put(interestChargedFromDateParamName, valueAsInput);
actualChanges.put("dateFormat", dateFormatAsInput);
actualChanges.put("locale", localeAsInput);
actualChanges.put("recalculateLoanSchedule", true);
final LocalDate newValue = command.localDateValueOfParameterNamed(interestChargedFromDateParamName);
if (newValue != null) {
this.interestChargedFromDate = Date.from(newValue.atStartOfDay(ZoneId.systemDefault()).toInstant());
} else {
this.interestChargedFromDate = null;
}
}
// the comparison should be done with the tenant date
// (DateUtils.getLocalDateOfTenant()) and not the server date (new
// LocalDate())
if (getSubmittedOnDate().isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan is submitted cannot be in the future.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, getSubmittedOnDate());
}
if (!(this.client == null)) {
if (getSubmittedOnDate().isBefore(this.client.getActivationLocalDate())) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage,
getSubmittedOnDate());
}
} else if (!(this.group == null)) {
if (getSubmittedOnDate().isBefore(this.group.getActivationLocalDate())) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage,
getSubmittedOnDate());
}
}
if (getSubmittedOnDate().isAfter(getExpectedDisbursedOnLocalDate())) {
final String errorMessage = "The date on which a loan is submitted cannot be after its expected disbursement date: "
+ getExpectedDisbursedOnLocalDate().toString();
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.after.expected.disbursement.date", errorMessage,
getSubmittedOnDate(), getExpectedDisbursedOnLocalDate());
}
final String chargesParamName = "charges";
if (isChargesModified) {
actualChanges.put(chargesParamName, getLoanCharges(possiblyModifedLoanCharges));
actualChanges.put("recalculateLoanSchedule", true);
}
final String collateralParamName = "collateral";
if (command.parameterExists(collateralParamName) && possiblyModifedLoanCollateralItems != null) {
if (possiblyModifedLoanCollateralItems.size() != 0) {
Set<LoanCollateralManagement> loanCollateralManagements = this.loanCollateralManagements;
boolean isTrue = possiblyModifedLoanCollateralItems.equals(loanCollateralManagements);
if (!isTrue) {
actualChanges.put(collateralParamName, getLoanCollateralDataFormCommand(possiblyModifedLoanCollateralItems));
}
}
}
final String loanTermFrequencyParamName = "loanTermFrequency";
if (command.isChangeInIntegerParameterNamed(loanTermFrequencyParamName, this.termFrequency)) {
final Integer newValue = command.integerValueOfParameterNamed(loanTermFrequencyParamName);
actualChanges.put(externalIdParamName, newValue);
this.termFrequency = newValue;
}
final String loanTermFrequencyTypeParamName = "loanTermFrequencyType";
if (command.isChangeInIntegerParameterNamed(loanTermFrequencyTypeParamName, this.termPeriodFrequencyType)) {
final Integer newValue = command.integerValueOfParameterNamed(loanTermFrequencyTypeParamName);
final PeriodFrequencyType newTermPeriodFrequencyType = PeriodFrequencyType.fromInt(newValue);
actualChanges.put(loanTermFrequencyTypeParamName, newTermPeriodFrequencyType.getValue());
this.termPeriodFrequencyType = newValue;
}
final String principalParamName = "principal";
if (command.isChangeInBigDecimalParameterNamed(principalParamName, this.approvedPrincipal)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(principalParamName);
this.approvedPrincipal = newValue;
}
if (command.isChangeInBigDecimalParameterNamed(principalParamName, this.proposedPrincipal)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(principalParamName);
this.proposedPrincipal = newValue;
}
if (loanProduct.isMultiDisburseLoan()) {
updateDisbursementDetails(command, actualChanges);
if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName,
this.maxOutstandingLoanBalance)) {
this.maxOutstandingLoanBalance = command
.bigDecimalValueOfParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName);
}
final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName);
if (loanProduct.isDisallowExpectedDisbursements()) {
if (disbursementDataArray != null) {
final String errorMessage = "For this loan product, disbursement details are not allowed";
throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
} else {
if (disbursementDataArray == null || disbursementDataArray.size() == 0) {
final String errorMessage = "For this loan product, disbursement details must be provided";
throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
}
if (disbursementDataArray.size() > loanProduct.maxTrancheCount()) {
final String errorMessage = "Number of tranche shouldn't be greter than " + loanProduct.maxTrancheCount();
throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage,
loanProduct.maxTrancheCount(), disbursementDetails.size());
}
} else {
this.disbursementDetails.clear();
}
if (loanProduct.isMultiDisburseLoan() || loanProduct.canDefineInstallmentAmount()) {
if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.emiAmountParameterName, this.fixedEmiAmount)) {
this.fixedEmiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.emiAmountParameterName);
actualChanges.put(LoanApiConstants.emiAmountParameterName, this.fixedEmiAmount);
actualChanges.put("recalculateLoanSchedule", true);
}
} else {
this.fixedEmiAmount = null;
}
if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName,
this.fixedPrincipalPercentagePerInstallment)) {
this.fixedPrincipalPercentagePerInstallment = command
.bigDecimalValueOfParameterNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName);
actualChanges.put(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName,
this.fixedPrincipalPercentagePerInstallment);
}
return actualChanges;
}
public void recalculateAllCharges() {
Set<LoanCharge> charges = this.charges();
int penaltyWaitPeriod = 0;
for (final LoanCharge loanCharge : charges) {
recalculateLoanCharge(loanCharge, penaltyWaitPeriod);
}
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
}
public boolean isInterestRecalculationEnabledForProduct() {
return this.loanProduct.isInterestRecalculationEnabled();
}
public boolean isMultiDisburmentLoan() {
return this.loanProduct.isMultiDisburseLoan();
}
/**
* Update interest recalculation settings if product configuration changes
*/
private void updateOverdueScheduleInstallment(final LoanCharge loanCharge) {
if (loanCharge.isOverdueInstallmentCharge() && loanCharge.isActive()) {
LoanOverdueInstallmentCharge overdueInstallmentCharge = loanCharge.getOverdueInstallmentCharge();
if (overdueInstallmentCharge != null) {
Integer installmentNumber = overdueInstallmentCharge.getInstallment().getInstallmentNumber();
LoanRepaymentScheduleInstallment installment = fetchRepaymentScheduleInstallment(installmentNumber);
overdueInstallmentCharge.updateLoanRepaymentScheduleInstallment(installment);
}
}
}
private void recalculateLoanCharge(final LoanCharge loanCharge, final int penaltyWaitPeriod) {
BigDecimal amount = BigDecimal.ZERO;
BigDecimal chargeAmt = BigDecimal.ZERO;
BigDecimal totalChargeAmt = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageBased()) {
if (loanCharge.isOverdueInstallmentCharge()) {
amount = calculateOverdueAmountPercentageAppliedTo(loanCharge, penaltyWaitPeriod);
} else {
amount = calculateAmountPercentageAppliedTo(loanCharge);
}
chargeAmt = loanCharge.getPercentage();
if (loanCharge.isInstalmentFee()) {
totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge);
}
} else {
chargeAmt = loanCharge.amountOrPercentage();
}
if (loanCharge.isActive()) {
loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt);
validateChargeHasValidSpecifiedDateIfApplicable(loanCharge, getDisbursementDate(), getLastRepaymentPeriodDueDate(false));
}
}
private BigDecimal calculateOverdueAmountPercentageAppliedTo(final LoanCharge loanCharge, final int penaltyWaitPeriod) {
LoanRepaymentScheduleInstallment installment = loanCharge.getOverdueInstallmentCharge().getInstallment();
LocalDate graceDate = DateUtils.getLocalDateOfTenant().minusDays(penaltyWaitPeriod);
Money amount = Money.zero(getCurrency());
if (graceDate.isAfter(installment.getDueDate())) {
amount = calculateOverdueAmountPercentageAppliedTo(installment, loanCharge.getChargeCalculation());
if (!amount.isGreaterThanZero()) {
loanCharge.setActive(false);
}
} else {
loanCharge.setActive(false);
}
return amount.getAmount();
}
private Money calculateOverdueAmountPercentageAppliedTo(LoanRepaymentScheduleInstallment installment,
ChargeCalculationType calculationType) {
Money amount = Money.zero(getCurrency());
switch (calculationType) {
case PERCENT_OF_AMOUNT:
amount = installment.getPrincipalOutstanding(getCurrency());
break;
case PERCENT_OF_AMOUNT_AND_INTEREST:
amount = installment.getPrincipalOutstanding(getCurrency()).plus(installment.getInterestOutstanding(getCurrency()));
break;
case PERCENT_OF_INTEREST:
amount = installment.getInterestOutstanding(getCurrency());
break;
default:
break;
}
return amount;
}
// This method returns date format and locale if present in the JsonCommand
private Map<String, String> getDateFormatAndLocale(final JsonCommand jsonCommand) {
Map<String, String> returnObject = new HashMap<>();
JsonElement jsonElement = jsonCommand.parsedJson();
if (jsonElement.isJsonObject()) {
JsonObject topLevel = jsonElement.getAsJsonObject();
if (topLevel.has(LoanApiConstants.dateFormatParameterName)
&& topLevel.get(LoanApiConstants.dateFormatParameterName).isJsonPrimitive()) {
final JsonPrimitive primitive = topLevel.get(LoanApiConstants.dateFormatParameterName).getAsJsonPrimitive();
returnObject.put(LoanApiConstants.dateFormatParameterName, primitive.getAsString());
}
if (topLevel.has(LoanApiConstants.localeParameterName)
&& topLevel.get(LoanApiConstants.localeParameterName).isJsonPrimitive()) {
final JsonPrimitive primitive = topLevel.get(LoanApiConstants.localeParameterName).getAsJsonPrimitive();
String localeString = primitive.getAsString();
returnObject.put(LoanApiConstants.localeParameterName, localeString);
}
}
return returnObject;
}
private Map<String, Object> parseDisbursementDetails(final JsonObject jsonObject, String dateFormat, Locale locale) {
Map<String, Object> returnObject = new HashMap<>();
if (jsonObject.get(LoanApiConstants.disbursementDateParameterName) != null
&& jsonObject.get(LoanApiConstants.disbursementDateParameterName).isJsonPrimitive()) {
final JsonPrimitive primitive = jsonObject.get(LoanApiConstants.disbursementDateParameterName).getAsJsonPrimitive();
final String valueAsString = primitive.getAsString();
if (StringUtils.isNotBlank(valueAsString)) {
LocalDate date = JsonParserHelper.convertFrom(valueAsString, LoanApiConstants.disbursementDateParameterName, dateFormat,
locale);
if (date != null) {
returnObject.put(LoanApiConstants.disbursementDateParameterName,
Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant()));
}
}
}
if (jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).isJsonPrimitive()
&& StringUtils.isNotBlank(jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).getAsString())) {
BigDecimal principal = jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementPrincipalParameterName).getAsBigDecimal();
returnObject.put(LoanApiConstants.disbursementPrincipalParameterName, principal);
}
if (jsonObject.has(LoanApiConstants.disbursementIdParameterName)
&& jsonObject.get(LoanApiConstants.disbursementIdParameterName).isJsonPrimitive()
&& StringUtils.isNotBlank(jsonObject.get(LoanApiConstants.disbursementIdParameterName).getAsString())) {
Long id = jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementIdParameterName).getAsLong();
returnObject.put(LoanApiConstants.disbursementIdParameterName, id);
}
if (jsonObject.has(LoanApiConstants.loanChargeIdParameterName)
&& jsonObject.get(LoanApiConstants.loanChargeIdParameterName).isJsonPrimitive()
&& StringUtils.isNotBlank(jsonObject.get(LoanApiConstants.loanChargeIdParameterName).getAsString())) {
returnObject.put(LoanApiConstants.loanChargeIdParameterName,
jsonObject.getAsJsonPrimitive(LoanApiConstants.loanChargeIdParameterName).getAsString());
}
return returnObject;
}
public void updateDisbursementDetails(final JsonCommand jsonCommand, final Map<String, Object> actualChanges) {
List<Long> disbursementList = fetchDisbursementIds();
List<Long> loanChargeIds = fetchLoanTrancheChargeIds();
int chargeIdLength = loanChargeIds.size();
String chargeIds = null;
// From modify application page, if user removes all charges, we should
// get empty array.
// So we need to remove all charges applied for this loan
boolean removeAllChages = false;
if (jsonCommand.parameterExists(LoanApiConstants.chargesParameterName)) {
JsonArray chargesArray = jsonCommand.arrayOfParameterNamed(LoanApiConstants.chargesParameterName);
if (chargesArray.size() == 0) {
removeAllChages = true;
}
}
if (jsonCommand.parameterExists(LoanApiConstants.disbursementDataParameterName)) {
final JsonArray disbursementDataArray = jsonCommand.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName);
if (disbursementDataArray != null && disbursementDataArray.size() > 0) {
String dateFormat = null;
Locale locale = null;
// Gets date format and locate
Map<String, String> dateAndLocale = getDateFormatAndLocale(jsonCommand);
dateFormat = dateAndLocale.get(LoanApiConstants.dateFormatParameterName);
if (dateAndLocale.containsKey(LoanApiConstants.localeParameterName)) {
locale = JsonParserHelper.localeFromString(dateAndLocale.get(LoanApiConstants.localeParameterName));
}
for (JsonElement jsonElement : disbursementDataArray) {
final JsonObject jsonObject = jsonElement.getAsJsonObject();
Map<String, Object> parsedDisbursementData = parseDisbursementDetails(jsonObject, dateFormat, locale);
Date expectedDisbursementDate = (Date) parsedDisbursementData.get(LoanApiConstants.disbursementDateParameterName);
BigDecimal principal = (BigDecimal) parsedDisbursementData.get(LoanApiConstants.disbursementPrincipalParameterName);
Long disbursementID = (Long) parsedDisbursementData.get(LoanApiConstants.disbursementIdParameterName);
chargeIds = (String) parsedDisbursementData.get(LoanApiConstants.loanChargeIdParameterName);
if (chargeIds != null) {
if (chargeIds.indexOf(",") != -1) {
Iterable<String> chargeId = Splitter.on(',').split(chargeIds);
for (String loanChargeId : chargeId) {
loanChargeIds.remove(Long.parseLong(loanChargeId));
}
} else {
loanChargeIds.remove(Long.parseLong(chargeIds));
}
}
createOrUpdateDisbursementDetails(disbursementID, actualChanges, expectedDisbursementDate, principal, disbursementList);
}
removeDisbursementAndAssociatedCharges(actualChanges, disbursementList, loanChargeIds, chargeIdLength, removeAllChages);
}
}
}
private void removeDisbursementAndAssociatedCharges(final Map<String, Object> actualChanges, List<Long> disbursementList,
List<Long> loanChargeIds, int chargeIdLength, boolean removeAllChages) {
if (removeAllChages) {
LoanCharge[] tempCharges = new LoanCharge[this.charges.size()];
this.charges.toArray(tempCharges);
for (LoanCharge loanCharge : tempCharges) {
removeLoanCharge(loanCharge);
}
this.trancheCharges.clear();
} else {
if (loanChargeIds.size() > 0 && loanChargeIds.size() != chargeIdLength) {
for (Long chargeId : loanChargeIds) {
LoanCharge deleteCharge = fetchLoanChargesById(chargeId);
if (this.charges.contains(deleteCharge)) {
removeLoanCharge(deleteCharge);
}
}
}
}
for (Long id : disbursementList) {
removeChargesByDisbursementID(id);
this.disbursementDetails.remove(fetchLoanDisbursementsById(id));
actualChanges.put("recalculateLoanSchedule", true);
}
}
private void createOrUpdateDisbursementDetails(Long disbursementID, final Map<String, Object> actualChanges,
Date expectedDisbursementDate, BigDecimal principal, List<Long> existingDisbursementList) {
if (disbursementID != null) {
LoanDisbursementDetails loanDisbursementDetail = fetchLoanDisbursementsById(disbursementID);
existingDisbursementList.remove(disbursementID);
if (loanDisbursementDetail.actualDisbursementDate() == null) {
Date actualDisbursementDate = null;
LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate,
principal, this.netDisbursalAmount);
disbursementDetails.updateLoan(this);
if (!loanDisbursementDetail.equals(disbursementDetails)) {
loanDisbursementDetail.copy(disbursementDetails);
actualChanges.put("disbursementDetailId", disbursementID);
actualChanges.put("recalculateLoanSchedule", true);
}
}
} else {
Date actualDisbursementDate = null;
LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate,
principal, this.netDisbursalAmount);
disbursementDetails.updateLoan(this);
this.disbursementDetails.add(disbursementDetails);
for (LoanTrancheCharge trancheCharge : trancheCharges) {
Charge chargeDefinition = trancheCharge.getCharge();
final LoanCharge loanCharge = LoanCharge.createNewWithoutLoan(chargeDefinition, principal, null, null, null,
LocalDate.ofInstant(expectedDisbursementDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()), null, null);
loanCharge.update(this);
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge,
disbursementDetails);
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
addLoanCharge(loanCharge);
}
actualChanges.put(LoanApiConstants.disbursementDataParameterName, expectedDisbursementDate + "-" + principal);
actualChanges.put("recalculateLoanSchedule", true);
}
}
private void removeChargesByDisbursementID(Long id) {
List<LoanCharge> tempCharges = new ArrayList<>();
for (LoanCharge charge : this.charges) {
LoanTrancheDisbursementCharge transCharge = charge.getTrancheDisbursementCharge();
if (transCharge != null && id.equals(transCharge.getloanDisbursementDetails().getId())) {
tempCharges.add(charge);
}
}
for (LoanCharge charge : tempCharges) {
removeLoanCharge(charge);
}
}
private List<Long> fetchLoanTrancheChargeIds() {
List<Long> list = new ArrayList<>();
for (LoanCharge charge : this.charges) {
if (charge.isTrancheDisbursementCharge() && charge.isActive()) {
list.add(charge.getId());
}
}
return list;
}
public LoanDisbursementDetails fetchLoanDisbursementsById(Long id) {
LoanDisbursementDetails loanDisbursementDetail = null;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
if (id.equals(disbursementDetail.getId())) {
loanDisbursementDetail = disbursementDetail;
break;
}
}
return loanDisbursementDetail;
}
private List<Long> fetchDisbursementIds() {
List<Long> list = new ArrayList<>();
for (LoanDisbursementDetails disbursementDetails : this.disbursementDetails) {
list.add(disbursementDetails.getId());
}
return list;
}
private LoanCollateralManagementData[] getLoanCollateralDataFormCommand(final Set<LoanCollateralManagement> setOfLoanCollateral) {
LoanCollateralManagementData[] existingLoanCollateral = null;
final List<LoanCollateralManagementData> loanCollateralList = new ArrayList<>();
for (final LoanCollateralManagement loanCollateral : setOfLoanCollateral) {
loanCollateralList.add(loanCollateral.toCommand());
}
existingLoanCollateral = loanCollateralList.toArray(new LoanCollateralManagementData[loanCollateralList.size()]);
return existingLoanCollateral;
}
private LoanChargeCommand[] getLoanCharges(final Set<LoanCharge> setOfLoanCharges) {
LoanChargeCommand[] existingLoanCharges = null;
final List<LoanChargeCommand> loanChargesList = new ArrayList<>();
for (final LoanCharge loanCharge : setOfLoanCharges) {
loanChargesList.add(loanCharge.toCommand());
}
existingLoanCharges = loanChargesList.toArray(new LoanChargeCommand[loanChargesList.size()]);
return existingLoanCharges;
}
private void removeFirstDisbursementTransaction() {
List<LoanTransaction> transactions = getLoanTransactions();
for (final LoanTransaction loanTransaction : transactions) {
if (loanTransaction.isDisbursement()) {
removeLoanTransaction(loanTransaction);
break;
}
}
}
public void loanApplicationSubmittal(final AppUser currentUser, final LoanScheduleModel loanSchedule,
final LoanApplicationTerms loanApplicationTerms, final LoanLifecycleStateMachine lifecycleStateMachine,
final LocalDate submittedOn, final String externalId, final boolean allowTransactionsOnHoliday, final List<Holiday> holidays,
final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) {
updateLoanSchedule(loanSchedule, currentUser);
LoanStatus from = null;
if (this.loanStatus != null) {
from = LoanStatus.fromInt(this.loanStatus);
}
final LoanStatus statusEnum = lifecycleStateMachine.transition(LoanEvent.LOAN_CREATED, from);
this.loanStatus = statusEnum.getValue();
this.externalId = externalId;
this.termFrequency = loanApplicationTerms.getLoanTermFrequency();
this.termPeriodFrequencyType = loanApplicationTerms.getLoanTermPeriodFrequencyType().getValue();
this.submittedOnDate = Date.from(submittedOn.atStartOfDay(ZoneId.systemDefault()).toInstant());
this.submittedBy = currentUser;
this.expectedDisbursementDate = Date
.from(loanApplicationTerms.getExpectedDisbursementDate().atStartOfDay(ZoneId.systemDefault()).toInstant());
this.expectedFirstRepaymentOnDate = loanApplicationTerms.getRepaymentStartFromDate();
this.interestChargedFromDate = loanApplicationTerms.getInterestChargedFromDate();
updateLoanScheduleDependentDerivedFields();
if (submittedOn.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan is submitted cannot be in the future.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn);
}
if (this.client != null && this.client.isActivatedAfter(submittedOn)) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage,
submittedOn);
}
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_CREATED, submittedOn);
if (this.group != null && this.group.isActivatedAfter(submittedOn)) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, submittedOn);
}
if (submittedOn.isAfter(getExpectedDisbursedOnLocalDate())) {
final String errorMessage = "The date on which a loan is submitted cannot be after its expected disbursement date: "
+ getExpectedDisbursedOnLocalDate().toString();
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.after.expected.disbursement.date", errorMessage,
submittedOn, getExpectedDisbursedOnLocalDate());
}
// charges are optional
int penaltyWaitPeriod = 0;
for (final LoanCharge loanCharge : charges()) {
recalculateLoanCharge(loanCharge, penaltyWaitPeriod);
}
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
// validate if disbursement date is a holiday or a non-working day
validateDisbursementDateIsOnNonWorkingDay(workingDays, allowTransactionsOnNonWorkingDay);
validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays);
/**
* Copy interest recalculation settings if interest recalculation is enabled
*/
if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) {
this.loanInterestRecalculationDetails = LoanInterestRecalculationDetails
.createFrom(this.loanProduct.getProductInterestRecalculationDetails());
this.loanInterestRecalculationDetails.updateLoan(this);
}
}
private LocalDate determineExpectedMaturityDate() {
final int numberOfInstallments = this.repaymentScheduleInstallments.size();
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
LocalDate maturityDate = installments.get(numberOfInstallments - 1).getDueDate();
ListIterator<LoanRepaymentScheduleInstallment> iterator = installments.listIterator(numberOfInstallments);
while (iterator.hasPrevious()) {
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = iterator.previous();
if (!loanRepaymentScheduleInstallment.isRecalculatedInterestComponent()) {
maturityDate = loanRepaymentScheduleInstallment.getDueDate();
break;
}
}
return maturityDate;
}
public Map<String, Object> loanApplicationRejection(final AppUser currentUser, final JsonCommand command,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
validateAccountStatus(LoanEvent.LOAN_REJECTED);
final Map<String, Object> actualChanges = new LinkedHashMap<>();
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, LoanStatus.fromInt(this.loanStatus));
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
final LocalDate rejectedOn = command.localDateValueOfParameterNamed("rejectedOnDate");
final Locale locale = new Locale(command.locale());
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
this.rejectedOnDate = Date.from(rejectedOn.atStartOfDay(ZoneId.systemDefault()).toInstant());
this.rejectedBy = currentUser;
this.closedOnDate = Date.from(rejectedOn.atStartOfDay(ZoneId.systemDefault()).toInstant());
this.closedBy = currentUser;
actualChanges.put("locale", command.locale());
actualChanges.put("dateFormat", command.dateFormat());
actualChanges.put("rejectedOnDate", rejectedOn.format(fmt));
actualChanges.put("closedOnDate", rejectedOn.format(fmt));
if (rejectedOn.isBefore(getSubmittedOnDate())) {
final String errorMessage = "The date on which a loan is rejected cannot be before its submittal date: "
+ getSubmittedOnDate().toString();
throw new InvalidLoanStateTransitionException("reject", "cannot.be.before.submittal.date", errorMessage, rejectedOn,
getSubmittedOnDate());
}
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REJECTED, rejectedOn);
if (rejectedOn.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan is rejected cannot be in the future.";
throw new InvalidLoanStateTransitionException("reject", "cannot.be.a.future.date", errorMessage, rejectedOn);
}
} else {
final String errorMessage = "Only the loan applications with status 'Submitted and pending approval' are allowed to be rejected.";
throw new InvalidLoanStateTransitionException("reject", "cannot.reject", errorMessage);
}
return actualChanges;
}
public Map<String, Object> loanApplicationWithdrawnByApplicant(final AppUser currentUser, final JsonCommand command,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
final Map<String, Object> actualChanges = new LinkedHashMap<>();
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAWN, LoanStatus.fromInt(this.loanStatus));
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
LocalDate withdrawnOn = command.localDateValueOfParameterNamed("withdrawnOnDate");
if (withdrawnOn == null) {
withdrawnOn = command.localDateValueOfParameterNamed("eventDate");
}
final Locale locale = new Locale(command.locale());
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
this.withdrawnOnDate = Date.from(withdrawnOn.atStartOfDay(ZoneId.systemDefault()).toInstant());
this.withdrawnBy = currentUser;
this.closedOnDate = Date.from(withdrawnOn.atStartOfDay(ZoneId.systemDefault()).toInstant());
this.closedBy = currentUser;
actualChanges.put("locale", command.locale());
actualChanges.put("dateFormat", command.dateFormat());
actualChanges.put("withdrawnOnDate", withdrawnOn.format(fmt));
actualChanges.put("closedOnDate", withdrawnOn.format(fmt));
if (withdrawnOn.isBefore(getSubmittedOnDate())) {
final String errorMessage = "The date on which a loan is withdrawn cannot be before its submittal date: "
+ getSubmittedOnDate().toString();
throw new InvalidLoanStateTransitionException("withdraw", "cannot.be.before.submittal.date", errorMessage, command,
getSubmittedOnDate());
}
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_WITHDRAWN, withdrawnOn);
if (withdrawnOn.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan is withdrawn cannot be in the future.";
throw new InvalidLoanStateTransitionException("withdraw", "cannot.be.a.future.date", errorMessage, command);
}
} else {
final String errorMessage = "Only the loan applications with status 'Submitted and pending approval' are allowed to be withdrawn by applicant.";
throw new InvalidLoanStateTransitionException("withdraw", "cannot.withdraw", errorMessage);
}
return actualChanges;
}
public Map<String, Object> loanApplicationApproval(final AppUser currentUser, final JsonCommand command,
final JsonArray disbursementDataArray, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
validateAccountStatus(LoanEvent.LOAN_APPROVED);
final Map<String, Object> actualChanges = new LinkedHashMap<>();
/*
* statusEnum is holding the possible new status derived from loanLifecycleStateMachine.transition.
*/
final LoanStatus newStatusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVED, LoanStatus.fromInt(this.loanStatus));
/*
* FIXME: There is no need to check below condition, if loanLifecycleStateMachine.transition is doing it's
* responsibility properly. Better implementation approach is, if code passes invalid combination of states
* (fromState and toState), state machine should return invalidate state and below if condition should check for
* not equal to invalidateState, instead of check new value is same as present value.
*/
if (!newStatusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = newStatusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
// only do below if status has changed in the 'approval' case
LocalDate approvedOn = command.localDateValueOfParameterNamed("approvedOnDate");
String approvedOnDateChange = command.stringValueOfParameterNamed("approvedOnDate");
if (approvedOn == null) {
approvedOn = command.localDateValueOfParameterNamed("eventDate");
approvedOnDateChange = command.stringValueOfParameterNamed("eventDate");
}
LocalDate expecteddisbursementDate = command.localDateValueOfParameterNamed("expectedDisbursementDate");
BigDecimal approvedLoanAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.approvedLoanAmountParameterName);
if (approvedLoanAmount != null) {
compareApprovedToProposedPrincipal(approvedLoanAmount);
/*
* All the calculations are done based on the principal amount, so it is necessary to set principal
* amount to approved amount
*/
this.approvedPrincipal = approvedLoanAmount;
this.loanRepaymentScheduleDetail.setPrincipal(approvedLoanAmount);
actualChanges.put(LoanApiConstants.approvedLoanAmountParameterName, approvedLoanAmount);
actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName, approvedLoanAmount);
actualChanges.put(LoanApiConstants.disbursementNetDisbursalAmountParameterName, netDisbursalAmount);
/* Update disbursement details */
if (disbursementDataArray != null) {
updateDisbursementDetails(command, actualChanges);
}
}
recalculateAllCharges();
if (loanProduct.isMultiDisburseLoan()) {
if (loanProduct.isDisallowExpectedDisbursements()) {
if (!disbursementDetails.isEmpty()) {
final String errorMessage = "For this loan product, disbursement details are not allowed";
throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
} else {
if (disbursementDetails.isEmpty()) {
final String errorMessage = "For this loan product, disbursement details must be provided";
throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
}
if (this.disbursementDetails.size() > loanProduct.maxTrancheCount()) {
final String errorMessage = "Number of tranche shouldn't be greter than " + loanProduct.maxTrancheCount();
throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage,
loanProduct.maxTrancheCount(), disbursementDetails.size());
}
}
this.approvedOnDate = Date.from(approvedOn.atStartOfDay(ZoneId.systemDefault()).toInstant());
this.approvedBy = currentUser;
actualChanges.put("locale", command.locale());
actualChanges.put("dateFormat", command.dateFormat());
actualChanges.put("approvedOnDate", approvedOnDateChange);
final LocalDate submittalDate = LocalDate.ofInstant(this.submittedOnDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
if (approvedOn.isBefore(submittalDate)) {
final String errorMessage = "The date on which a loan is approved cannot be before its submittal date: "
+ submittalDate.toString();
throw new InvalidLoanStateTransitionException("approval", "cannot.be.before.submittal.date", errorMessage,
getApprovedOnDate(), submittalDate);
}
if (expecteddisbursementDate != null) {
this.expectedDisbursementDate = Date.from(expecteddisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
actualChanges.put("expectedDisbursementDate", expectedDisbursementDate);
if (expecteddisbursementDate.isBefore(approvedOn)) {
final String errorMessage = "The expected disbursement date should be either on or after the approval date: "
+ approvedOn.toString();
throw new InvalidLoanStateTransitionException("expecteddisbursal", "should.be.on.or.after.approval.date", errorMessage,
getApprovedOnDate(), expecteddisbursementDate);
}
}
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_APPROVED, approvedOn);
if (approvedOn.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan is approved cannot be in the future.";
throw new InvalidLoanStateTransitionException("approval", "cannot.be.a.future.date", errorMessage, getApprovedOnDate());
}
if (this.loanOfficer != null) {
final LoanOfficerAssignmentHistory loanOfficerAssignmentHistory = LoanOfficerAssignmentHistory.createNew(this,
this.loanOfficer, approvedOn);
this.loanOfficerHistory.add(loanOfficerAssignmentHistory);
}
this.adjustNetDisbursalAmount(this.approvedPrincipal);
}
return actualChanges;
}
private void compareApprovedToProposedPrincipal(BigDecimal approvedLoanAmount) {
if (this.loanProduct().isDisallowExpectedDisbursements() && this.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
BigDecimal maxApprovedLoanAmount = getOverAppliedMax();
if (approvedLoanAmount.compareTo(maxApprovedLoanAmount) > 0) {
final String errorMessage = "Loan approved amount can't be greater than maximum applied loan amount calculation.";
throw new InvalidLoanStateTransitionException("approval",
"amount.can't.be.greater.than.maximum.applied.loan.amount.calculation", errorMessage, approvedLoanAmount,
maxApprovedLoanAmount);
}
} else {
if (approvedLoanAmount.compareTo(this.proposedPrincipal) > 0) {
final String errorMessage = "Loan approved amount can't be greater than loan amount demanded.";
throw new InvalidLoanStateTransitionException("approval", "amount.can't.be.greater.than.loan.amount.demanded", errorMessage,
this.proposedPrincipal, approvedLoanAmount);
}
}
}
private BigDecimal getOverAppliedMax() {
BigDecimal maxAmount = null;
if (this.getLoanProduct().getOverAppliedCalculationType().equals("percentage")) {
BigDecimal overAppliedNumber = BigDecimal.valueOf(getLoanProduct().getOverAppliedNumber());
BigDecimal x = overAppliedNumber.divide(BigDecimal.valueOf(100));
BigDecimal totalPercentage = BigDecimal.valueOf(1).add(x);
maxAmount = this.proposedPrincipal.multiply(totalPercentage);
} else {
maxAmount = this.proposedPrincipal.add(BigDecimal.valueOf(getLoanProduct().getOverAppliedNumber()));
}
return maxAmount;
}
public Map<String, Object> undoApproval(final LoanLifecycleStateMachine loanLifecycleStateMachine) {
validateAccountStatus(LoanEvent.LOAN_APPROVAL_UNDO);
final Map<String, Object> actualChanges = new LinkedHashMap<>();
final LoanStatus currentStatus = LoanStatus.fromInt(this.loanStatus);
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVAL_UNDO, currentStatus);
if (!statusEnum.hasStateOf(currentStatus)) {
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
this.approvedOnDate = null;
this.approvedBy = null;
if (this.approvedPrincipal.compareTo(this.proposedPrincipal) != 0) {
this.approvedPrincipal = this.proposedPrincipal;
this.loanRepaymentScheduleDetail.setPrincipal(this.proposedPrincipal);
actualChanges.put(LoanApiConstants.approvedLoanAmountParameterName, this.proposedPrincipal);
actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName, this.proposedPrincipal);
}
actualChanges.put("approvedOnDate", "");
this.loanOfficerHistory.clear();
}
return actualChanges;
}
public Collection<Long> findExistingTransactionIds() {
final Collection<Long> ids = new ArrayList<>();
List<LoanTransaction> transactions = getLoanTransactions();
for (final LoanTransaction transaction : transactions) {
ids.add(transaction.getId());
}
return ids;
}
public Collection<Long> findExistingReversedTransactionIds() {
final Collection<Long> ids = new ArrayList<>();
List<LoanTransaction> transactions = getLoanTransactions();
for (final LoanTransaction transaction : transactions) {
if (transaction.isReversed()) {
ids.add(transaction.getId());
}
}
return ids;
}
public ChangedTransactionDetail disburse(final AppUser currentUser, final JsonCommand command, final Map<String, Object> actualChanges,
final ScheduleGeneratorDTO scheduleGeneratorDTO, final PaymentDetail paymentDetail) {
final LoanStatus statusEnum = this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED,
LoanStatus.fromInt(this.loanStatus));
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
this.disbursedBy = currentUser;
updateLoanScheduleDependentDerivedFields();
actualChanges.put("locale", command.locale());
actualChanges.put("dateFormat", command.dateFormat());
actualChanges.put("actualDisbursementDate", command.stringValueOfParameterNamed("actualDisbursementDate"));
HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO();
// validate if disbursement date is a holiday or a non-working day
validateDisbursementDateIsOnNonWorkingDay(holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
validateDisbursementDateIsOnHoliday(holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()
&& (fetchRepaymentScheduleInstallment(1).getDueDate().isBefore(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))
|| isDisbursementMissed())) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO, currentUser);
}
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
LocalDateTime createdDate = DateUtils.getLocalDateTimeOfTenant();
handleDisbursementTransaction(actualDisbursementDate, createdDate, currentUser, paymentDetail);
updateLoanSummaryDerivedFields();
final Money interestApplied = Money.of(getCurrency(), this.summary.getTotalInterestCharged());
/**
* Add an interest applied transaction of the interest is accrued upfront (Up front accrual), no accounting or
* cash based accounting is selected
**/
if (((isMultiDisburmentLoan() && getDisbursedLoanDisbursementDetails().size() == 1) || !isMultiDisburmentLoan())
&& isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) {
final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(getOffice(), this, interestApplied,
actualDisbursementDate, createdDate, currentUser);
addLoanTransaction(interestAppliedTransaction);
}
return reprocessTransactionForDisbursement();
}
private List<LoanDisbursementDetails> getDisbursedLoanDisbursementDetails() {
List<LoanDisbursementDetails> ret = new ArrayList<>();
if (this.disbursementDetails != null && this.disbursementDetails.size() > 0) {
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
if (disbursementDetail.actualDisbursementDate() != null) {
ret.add(disbursementDetail);
}
}
}
return ret;
}
public void regenerateScheduleOnDisbursement(final ScheduleGeneratorDTO scheduleGeneratorDTO, final boolean recalculateSchedule,
final LocalDate actualDisbursementDate, BigDecimal emiAmount, final AppUser currentUser, LocalDate nextPossibleRepaymentDate,
Date rescheduledRepaymentDate) {
boolean isEmiAmountChanged = false;
if ((this.loanProduct.isMultiDisburseLoan() || this.loanProduct.canDefineInstallmentAmount()) && emiAmount != null
&& emiAmount.compareTo(retriveLastEmiAmount()) != 0) {
if (this.loanProduct.isMultiDisburseLoan()) {
final Date dateValue = null;
final boolean isSpecificToInstallment = false;
final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = scheduleGeneratorDTO
.isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled();
Date effectiveDateFrom = Date.from(actualDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
if (!isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled && actualDisbursementDate.equals(nextPossibleRepaymentDate)) {
effectiveDateFrom = Date.from(nextPossibleRepaymentDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
}
LoanTermVariations loanVariationTerms = new LoanTermVariations(LoanTermVariationType.EMI_AMOUNT.getValue(),
effectiveDateFrom, emiAmount, dateValue, isSpecificToInstallment, this, LoanStatus.ACTIVE.getValue());
this.loanTermVariations.add(loanVariationTerms);
} else {
this.fixedEmiAmount = emiAmount;
}
isEmiAmountChanged = true;
}
if (rescheduledRepaymentDate != null && this.loanProduct.isMultiDisburseLoan()) {
final boolean isSpecificToInstallment = false;
LoanTermVariations loanVariationTerms = new LoanTermVariations(LoanTermVariationType.DUE_DATE.getValue(),
Date.from(nextPossibleRepaymentDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), emiAmount,
rescheduledRepaymentDate, isSpecificToInstallment, this, LoanStatus.ACTIVE.getValue());
this.loanTermVariations.add(loanVariationTerms);
}
if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate) || recalculateSchedule || isEmiAmountChanged
|| rescheduledRepaymentDate != null) {
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO, currentUser);
} else {
regenerateRepaymentSchedule(scheduleGeneratorDTO, currentUser);
}
}
}
public boolean canDisburse(final LocalDate actualDisbursementDate) {
Date lastDusburseDate = this.actualDisbursementDate;
final LoanStatus statusEnum = this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED,
LoanStatus.fromInt(this.loanStatus));
boolean isMultiTrancheDisburse = false;
if (LoanStatus.fromInt(this.loanStatus).isActive() && isAllTranchesNotDisbursed()) {
LoanDisbursementDetails details = fetchLastDisburseDetail();
if (details != null) {
lastDusburseDate = details.actualDisbursementDate();
}
if (Date.from(actualDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant()).before(lastDusburseDate)) {
final String errorMsg = "Loan can't be disbursed before " + lastDusburseDate;
throw new LoanDisbursalException(errorMsg, "actualdisbursementdate.before.lastdusbursedate", lastDusburseDate,
Date.from(actualDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
}
isMultiTrancheDisburse = true;
}
return !statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus)) || isMultiTrancheDisburse;
}
public Money adjustDisburseAmount(final JsonCommand command, final LocalDate actualDisbursementDate) {
Money disburseAmount = this.loanRepaymentScheduleDetail.getPrincipal().zero();
BigDecimal principalDisbursed = command.bigDecimalValueOfParameterNamed(LoanApiConstants.principalDisbursedParameterName);
if (this.actualDisbursementDate == null) {
this.actualDisbursementDate = Date.from(actualDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
}
BigDecimal diff = BigDecimal.ZERO;
Collection<LoanDisbursementDetails> details = fetchUndisbursedDetail();
if (principalDisbursed == null) {
disburseAmount = this.loanRepaymentScheduleDetail.getPrincipal();
if (!details.isEmpty()) {
disburseAmount = disburseAmount.zero();
for (LoanDisbursementDetails disbursementDetails : details) {
disbursementDetails.updateActualDisbursementDate(
Date.from(actualDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
disburseAmount = disburseAmount.plus(disbursementDetails.principal());
}
}
} else {
if (this.loanProduct.isMultiDisburseLoan()) {
disburseAmount = Money.of(getCurrency(), principalDisbursed);
} else {
disburseAmount = disburseAmount.plus(principalDisbursed);
}
if (details.isEmpty()) {
diff = this.loanRepaymentScheduleDetail.getPrincipal().minus(principalDisbursed).getAmount();
} else {
for (LoanDisbursementDetails disbursementDetails : details) {
disbursementDetails.updateActualDisbursementDate(
Date.from(actualDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
disbursementDetails.updatePrincipal(principalDisbursed);
}
}
if (this.loanProduct().isMultiDisburseLoan()) {
Collection<LoanDisbursementDetails> loanDisburseDetails = this.getDisbursementDetails();
BigDecimal setPrincipalAmount = BigDecimal.ZERO;
BigDecimal totalAmount = BigDecimal.ZERO;
for (LoanDisbursementDetails disbursementDetails : loanDisburseDetails) {
if (disbursementDetails.actualDisbursementDate() != null) {
setPrincipalAmount = setPrincipalAmount.add(disbursementDetails.principal());
}
totalAmount = totalAmount.add(disbursementDetails.principal());
}
this.loanRepaymentScheduleDetail.setPrincipal(setPrincipalAmount);
compareDisbursedToApprovedOrProposedPrincipal(disburseAmount.getAmount(), totalAmount);
} else {
this.loanRepaymentScheduleDetail.setPrincipal(this.loanRepaymentScheduleDetail.getPrincipal().minus(diff).getAmount());
}
if (!this.loanProduct().isMultiDisburseLoan() && diff.compareTo(BigDecimal.ZERO) < 0) {
final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved amount ";
throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.amount", principalDisbursed,
this.loanRepaymentScheduleDetail.getPrincipal().getAmount());
}
}
return disburseAmount;
}
private void compareDisbursedToApprovedOrProposedPrincipal(BigDecimal disbursedAmount, BigDecimal totalDisbursed) {
if (this.loanProduct().isDisallowExpectedDisbursements() && this.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
BigDecimal maxDisbursedAmount = getOverAppliedMax();
if (disbursedAmount.compareTo(maxDisbursedAmount) > 0) {
final String errorMessage = "Loan disbursal amount can't be greater than maximum applied loan amount calculation.";
throw new InvalidLoanStateTransitionException("disbursal",
"amount.can't.be.greater.than.maximum.applied.loan.amount.calculation", errorMessage, disbursedAmount,
maxDisbursedAmount);
}
} else {
if (totalDisbursed.compareTo(this.approvedPrincipal) > 0) {
final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved principal ";
throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.principal", totalDisbursed,
this.approvedPrincipal);
}
}
}
private ChangedTransactionDetail reprocessTransactionForDisbursement() {
ChangedTransactionDetail changedTransactionDetail = null;
if (this.loanProduct.isMultiDisburseLoan()) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), charges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
}
updateLoanSummaryDerivedFields();
}
return changedTransactionDetail;
}
private Collection<LoanDisbursementDetails> fetchUndisbursedDetail() {
Collection<LoanDisbursementDetails> disbursementDetails = new ArrayList<>();
Date date = null;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
if (disbursementDetail.actualDisbursementDate() == null) {
if (date == null || disbursementDetail.expectedDisbursementDate().compareTo(date) == 0 ? Boolean.TRUE : Boolean.FALSE) {
disbursementDetails.add(disbursementDetail);
date = disbursementDetail.expectedDisbursementDate();
} else if (disbursementDetail.expectedDisbursementDate().before(date)) {
disbursementDetails.clear();
disbursementDetails.add(disbursementDetail);
date = disbursementDetail.expectedDisbursementDate();
}
}
}
return disbursementDetails;
}
private LoanDisbursementDetails fetchLastDisburseDetail() {
LoanDisbursementDetails details = null;
Date date = this.actualDisbursementDate;
if (date != null) {
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
if (disbursementDetail.actualDisbursementDate() != null) {
if (disbursementDetail.actualDisbursementDate().after(date)
|| disbursementDetail.actualDisbursementDate().compareTo(date) == 0 ? Boolean.TRUE : Boolean.FALSE) {
date = disbursementDetail.actualDisbursementDate();
details = disbursementDetail;
}
}
}
}
return details;
}
private boolean isDisbursementMissed() {
boolean isDisbursementMissed = false;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
if (disbursementDetail.actualDisbursementDate() == null && LocalDate.now(DateUtils.getDateTimeZoneOfTenant())
.isAfter(disbursementDetail.expectedDisbursementDateAsLocalDate())) {
isDisbursementMissed = true;
break;
}
}
return isDisbursementMissed;
}
public BigDecimal getDisbursedAmount() {
BigDecimal principal = BigDecimal.ZERO;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
if (disbursementDetail.actualDisbursementDate() != null) {
principal = principal.add(disbursementDetail.principal());
}
}
return principal;
}
private void removeDisbursementDetail() {
Set<LoanDisbursementDetails> details = new HashSet<>(this.disbursementDetails);
for (LoanDisbursementDetails disbursementDetail : details) {
if (disbursementDetail.actualDisbursementDate() == null) {
this.disbursementDetails.remove(disbursementDetail);
}
}
}
private boolean isDisbursementAllowed() {
boolean isAllowed = false;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
if (disbursementDetail.actualDisbursementDate() == null) {
isAllowed = true;
break;
}
}
return isAllowed;
}
private boolean atleastOnceDisbursed() {
boolean isDisbursed = false;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
if (disbursementDetail.actualDisbursementDate() != null) {
isDisbursed = true;
break;
}
}
return isDisbursed;
}
private void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment repaymentPeriod : installments) {
repaymentPeriod.updateDerivedFields(loanCurrency(), actualDisbursementDate);
}
}
/*
* Ability to regenerate the repayment schedule based on the loans current details/state.
*/
public void regenerateRepaymentSchedule(final ScheduleGeneratorDTO scheduleGeneratorDTO, final AppUser currentUser) {
final LoanScheduleModel loanSchedule = regenerateScheduleModel(scheduleGeneratorDTO);
if (loanSchedule == null) {
return;
}
updateLoanSchedule(loanSchedule, currentUser);
final Set<LoanCharge> charges = this.charges();
for (final LoanCharge loanCharge : charges) {
if (!loanCharge.isWaived()) {
recalculateLoanCharge(loanCharge, scheduleGeneratorDTO.getPenaltyWaitPeriod());
}
}
}
public LoanScheduleModel regenerateScheduleModel(final ScheduleGeneratorDTO scheduleGeneratorDTO) {
final RoundingMode roundingMode = MoneyHelper.getRoundingMode();
final MathContext mc = new MathContext(8, roundingMode);
final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(scheduleGeneratorDTO);
LoanScheduleGenerator loanScheduleGenerator = null;
if (loanApplicationTerms.isEqualAmortization()) {
if (loanApplicationTerms.getInterestMethod().isDecliningBalnce()) {
final LoanScheduleGenerator decliningLoanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory()
.create(InterestMethod.DECLINING_BALANCE);
Set<LoanCharge> loanCharges = charges();
LoanScheduleModel loanSchedule = decliningLoanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges,
scheduleGeneratorDTO.getHolidayDetailDTO());
loanApplicationTerms
.updateTotalInterestDue(Money.of(loanApplicationTerms.getCurrency(), loanSchedule.getTotalInterestCharged()));
}
loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(InterestMethod.FLAT);
} else {
loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(loanApplicationTerms.getInterestMethod());
}
final LoanScheduleModel loanSchedule = loanScheduleGenerator.generate(mc, loanApplicationTerms, charges(),
scheduleGeneratorDTO.getHolidayDetailDTO());
return loanSchedule;
}
private BigDecimal constructFloatingInterestRates(final BigDecimal annualNominalInterestRate, final FloatingRateDTO floatingRateDTO,
final List<LoanTermVariationsData> loanTermVariations) {
final LocalDate dateValue = null;
final boolean isSpecificToInstallment = false;
BigDecimal interestRate = annualNominalInterestRate;
if (loanProduct.isLinkedToFloatingInterestRate()) {
floatingRateDTO.resetInterestRateDiff();
Collection<FloatingRatePeriodData> applicableRates = loanProduct.fetchInterestRates(floatingRateDTO);
LocalDate interestRateStartDate = DateUtils.getLocalDateOfTenant();
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();
interestRate = periodData.getInterestRate();
}
loanTermVariations.add(loanTermVariation);
}
}
return interestRate;
}
private void handleDisbursementTransaction(final LocalDate disbursedOn, final LocalDateTime createdDate, final AppUser currentUser,
final PaymentDetail paymentDetail) {
// add repayment transaction to track incoming money from client to mfi
// for (charges due at time of disbursement)
/***
* TODO Vishwas: do we need to be able to pass in payment type details for repayments at disbursements too?
***/
final Money totalFeeChargesDueAtDisbursement = this.summary.getTotalFeeChargesDueAtDisbursement(loanCurrency());
/**
* all Charges repaid at disbursal is marked as repaid and "APPLY Charge" transactions are created for all other
* fees ( which are created during disbursal but not repaid)
**/
Money disbursentMoney = Money.zero(getCurrency());
final LoanTransaction chargesPayment = LoanTransaction.repaymentAtDisbursement(getOffice(), disbursentMoney, paymentDetail,
disbursedOn, null, createdDate, currentUser);
final Integer installmentNumber = null;
for (final LoanCharge charge : charges()) {
Date actualDisbursementDate = getActualDisbursementDate(charge);
if ((charge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue())
&& disbursedOn.equals(LocalDate.ofInstant(actualDisbursementDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()))
&& (actualDisbursementDate != null) && !charge.isWaived() && !charge.isFullyPaid())
|| (charge.getCharge().getChargeTimeType().equals(ChargeTimeType.TRANCHE_DISBURSEMENT.getValue())
&& disbursedOn
.equals(LocalDate.ofInstant(actualDisbursementDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()))
&& (actualDisbursementDate != null) && !charge.isWaived() && !charge.isFullyPaid())) {
if (totalFeeChargesDueAtDisbursement.isGreaterThanZero() && !charge.getChargePaymentMode().isPaymentModeAccountTransfer()) {
charge.markAsFullyPaid();
// Add "Loan Charge Paid By" details to this transaction
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount(),
installmentNumber);
chargesPayment.getLoanChargesPaid().add(loanChargePaidBy);
disbursentMoney = disbursentMoney.plus(charge.amount());
}
} else if (disbursedOn
.equals(LocalDate.ofInstant(this.actualDisbursementDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()))) {
/**
* create a Charge applied transaction if Up front Accrual, None or Cash based accounting is enabled
**/
if (isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) {
handleChargeAppliedTransaction(charge, disbursedOn, currentUser);
}
}
}
if (disbursentMoney.isGreaterThanZero()) {
final Money zero = Money.zero(getCurrency());
chargesPayment.updateComponentsAndTotal(zero, zero, disbursentMoney, zero);
chargesPayment.updateLoan(this);
addLoanTransaction(chargesPayment);
updateLoanOutstandingBalaces();
}
if (getApprovedOnDate() != null && disbursedOn.isBefore(getApprovedOnDate())) {
final String errorMessage = "The date on which a loan is disbursed cannot be before its approval date: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.before.approval.date", errorMessage, disbursedOn,
getApprovedOnDate());
}
if (getExpectedFirstRepaymentOnDate() != null
&& (disbursedOn.isAfter(this.fetchRepaymentScheduleInstallment(1).getDueDate())
|| disbursedOn.isAfter(getExpectedFirstRepaymentOnDate()))
&& Date.from(disbursedOn.atStartOfDay(ZoneId.systemDefault()).toInstant()).compareTo(this.actualDisbursementDate) == 0
? Boolean.TRUE
: Boolean.FALSE) {
final String errorMessage = "submittedOnDate cannot be after the loans expectedFirstRepaymentOnDate: "
+ getExpectedFirstRepaymentOnDate().toString();
throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.expected.first.repayment.date", errorMessage,
disbursedOn, getExpectedFirstRepaymentOnDate());
}
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_DISBURSED, disbursedOn);
if (disbursedOn.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber
+ " is disbursed cannot be in the future.";
throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn);
}
}
public LoanTransaction handlePayDisbursementTransaction(final Long chargeId, final LoanTransaction chargesPayment,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
LoanCharge charge = null;
for (final LoanCharge loanCharge : this.charges) {
if (loanCharge.isActive() && chargeId.equals(loanCharge.getId())) {
charge = loanCharge;
}
}
@SuppressWarnings("null")
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount(), null);
chargesPayment.getLoanChargesPaid().add(loanChargePaidBy);
final Money zero = Money.zero(getCurrency());
chargesPayment.updateComponents(zero, zero, charge.getAmount(getCurrency()), zero);
chargesPayment.updateLoan(this);
addLoanTransaction(chargesPayment);
updateLoanOutstandingBalaces();
charge.markAsFullyPaid();
return chargesPayment;
}
public void removePostDatedChecks() {
List<PostDatedChecks> postDatedChecks = new ArrayList<>();
this.postDatedChecks = postDatedChecks;
}
public Map<String, Object> undoDisbursal(final ScheduleGeneratorDTO scheduleGeneratorDTO, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, AppUser currentUser) {
validateAccountStatus(LoanEvent.LOAN_DISBURSAL_UNDO);
final Map<String, Object> actualChanges = new LinkedHashMap<>();
final LoanStatus currentStatus = LoanStatus.fromInt(this.loanStatus);
final LoanStatus statusEnum = this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSAL_UNDO, currentStatus);
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_DISBURSAL_UNDO, getDisbursementDate());
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
if (!statusEnum.hasStateOf(currentStatus)) {
this.loanStatus = statusEnum.getValue();
actualChanges.put("status", LoanEnumerations.status(this.loanStatus));
final LocalDate actualDisbursementDate = getDisbursementDate();
final boolean isScheduleRegenerateRequired = isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate);
this.actualDisbursementDate = null;
this.disbursedBy = null;
boolean isDisbursedAmountChanged = approvedPrincipal.compareTo(this.loanRepaymentScheduleDetail.getPrincipal().getAmount()) == 0
? Boolean.FALSE
: Boolean.TRUE;
this.loanRepaymentScheduleDetail.setPrincipal(this.approvedPrincipal);
if (this.loanProduct.isMultiDisburseLoan()) {
for (final LoanDisbursementDetails details : this.disbursementDetails) {
details.updateActualDisbursementDate(null);
}
}
boolean isEmiAmountChanged = this.loanTermVariations.size() > 0;
updateLoanToPreDisbursalState();
if (isScheduleRegenerateRequired || isDisbursedAmountChanged || isEmiAmountChanged
|| this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
// clear off actual disbusrement date so schedule regeneration
// uses expected date.
regenerateRepaymentSchedule(scheduleGeneratorDTO, currentUser);
if (isDisbursedAmountChanged) {
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
}
} else if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
for (final LoanRepaymentScheduleInstallment period : getRepaymentScheduleInstallments()) {
period.resetAccrualComponents();
}
}
if (this.isTopup) {
this.loanTopupDetails.setAccountTransferDetails(null);
this.loanTopupDetails.setTopupAmount(null);
}
this.adjustNetDisbursalAmount(this.approvedPrincipal);
actualChanges.put("actualDisbursementDate", "");
updateLoanSummaryDerivedFields();
}
return actualChanges;
}
private void reverseExistingTransactions() {
Collection<LoanTransaction> retainTransactions = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
transaction.reverse();
if (transaction.getId() != null) {
retainTransactions.add(transaction);
}
}
this.loanTransactions.retainAll(retainTransactions);
}
private void updateLoanToPreDisbursalState() {
this.actualDisbursementDate = null;
this.accruedTill = null;
reverseExistingTransactions();
for (final LoanCharge charge : charges()) {
if (charge.isOverdueInstallmentCharge()) {
charge.setActive(false);
} else {
charge.resetToOriginal(loanCurrency());
}
}
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
currentInstallment.resetDerivedComponents();
}
for (LoanTermVariations variations : this.loanTermVariations) {
if (variations.getOnLoanStatus().equals(LoanStatus.ACTIVE.getValue())) {
variations.markAsInactive();
}
}
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), charges());
updateLoanSummaryDerivedFields();
}
public ChangedTransactionDetail waiveInterest(final LoanTransaction waiveInterestTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO, final AppUser currentUser) {
validateAccountStatus(LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
waiveInterestTransaction.getTransactionDate());
validateActivityNotBeforeLastTransactionDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, waiveInterestTransaction.getTransactionDate());
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final ChangedTransactionDetail changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(waiveInterestTransaction,
loanLifecycleStateMachine, null, scheduleGeneratorDTO, currentUser);
return changedTransactionDetail;
}
@SuppressWarnings("null")
public ChangedTransactionDetail makeRepayment(final LoanTransaction repaymentTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, boolean isRecoveryRepayment, final ScheduleGeneratorDTO scheduleGeneratorDTO,
final AppUser currentUser, Boolean isHolidayValidationDone) {
HolidayDetailDTO holidayDetailDTO = null;
LoanEvent event = null;
if (isRecoveryRepayment) {
event = LoanEvent.LOAN_RECOVERY_PAYMENT;
} else {
event = LoanEvent.LOAN_REPAYMENT_OR_WAIVER;
}
if (!isHolidayValidationDone) {
holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO();
}
validateAccountStatus(event);
validateActivityNotBeforeClientOrGroupTransferDate(event, repaymentTransaction.getTransactionDate());
validateActivityNotBeforeLastTransactionDate(event, repaymentTransaction.getTransactionDate());
if (!isHolidayValidationDone) {
validateRepaymentDateIsOnHoliday(repaymentTransaction.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(),
holidayDetailDTO.getHolidays());
validateRepaymentDateIsOnNonWorkingDay(repaymentTransaction.getTransactionDate(), holidayDetailDTO.getWorkingDays(),
holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
}
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final ChangedTransactionDetail changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction,
loanLifecycleStateMachine, null, scheduleGeneratorDTO, currentUser);
return changedTransactionDetail;
}
public void makeChargePayment(final Long chargeId, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final HolidayDetailDTO holidayDetailDTO, final LoanTransaction paymentTransaction, final Integer installmentNumber) {
validateAccountStatus(LoanEvent.LOAN_CHARGE_PAYMENT);
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_CHARGE_PAYMENT, paymentTransaction.getTransactionDate());
validateActivityNotBeforeLastTransactionDate(LoanEvent.LOAN_CHARGE_PAYMENT, paymentTransaction.getTransactionDate());
validateRepaymentDateIsOnHoliday(paymentTransaction.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(),
holidayDetailDTO.getHolidays());
validateRepaymentDateIsOnNonWorkingDay(paymentTransaction.getTransactionDate(), holidayDetailDTO.getWorkingDays(),
holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
if (paymentTransaction.getTransactionDate().isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan charge paid cannot be in the future.";
throw new InvalidLoanStateTransitionException("charge.payment", "cannot.be.a.future.date", errorMessage,
paymentTransaction.getTransactionDate());
}
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
LoanCharge charge = null;
for (final LoanCharge loanCharge : this.charges) {
if (loanCharge.isActive() && chargeId.equals(loanCharge.getId())) {
charge = loanCharge;
}
}
handleChargePaidTransaction(charge, paymentTransaction, loanLifecycleStateMachine, installmentNumber);
}
public void makeRefund(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final boolean allowTransactionsOnHoliday, final List<Holiday> holidays, final WorkingDays workingDays,
final boolean allowTransactionsOnNonWorkingDay) {
validateRepaymentDateIsOnHoliday(loanTransaction.getTransactionDate(), allowTransactionsOnHoliday, holidays);
validateRepaymentDateIsOnNonWorkingDay(loanTransaction.getTransactionDate(), workingDays, allowTransactionsOnNonWorkingDay);
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
if (status().isOverpaid()) {
if (this.totalOverpaid.compareTo(loanTransaction.getAmount(getCurrency()).getAmount()) < 0) {
final String errorMessage = "The refund amount must be less than or equal to overpaid amount ";
throw new InvalidLoanStateTransitionException("transaction", "is.exceeding.overpaid.amount", errorMessage,
this.totalOverpaid, loanTransaction.getAmount(getCurrency()).getAmount());
} else if (!isAfterLatRepayment(loanTransaction, getLoanTransactions())) {
final String errorMessage = "Transfer funds is allowed only after last repayment date";
throw new InvalidLoanStateTransitionException("transaction", "is.not.after.repayment.date", errorMessage);
}
} else {
final String errorMessage = "Transfer funds is allowed only for loan accounts with overpaid status ";
throw new InvalidLoanStateTransitionException("transaction", "is.not.a.overpaid.loan", errorMessage);
}
loanTransaction.updateLoan(this);
if (loanTransaction.isNotZero(loanCurrency())) {
addLoanTransaction(loanTransaction);
}
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
}
private ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction,
final ScheduleGeneratorDTO scheduleGeneratorDTO, final AppUser currentUser) {
ChangedTransactionDetail changedTransactionDetail = null;
LoanStatus statusEnum = null;
LocalDate recalculateFrom = loanTransaction.getTransactionDate();
if (adjustedTransaction != null && adjustedTransaction.getTransactionDate().isBefore(recalculateFrom)) {
recalculateFrom = adjustedTransaction.getTransactionDate();
}
if (loanTransaction.isRecoveryRepayment()) {
statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_RECOVERY_PAYMENT, LoanStatus.fromInt(this.loanStatus));
} else {
statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, LoanStatus.fromInt(this.loanStatus));
}
if (loanTransaction.isRecoveryRepayment()
&& loanTransaction.getAmount(loanCurrency()).getAmount().compareTo(getSummary().getTotalWrittenOff()) > 0) {
final String errorMessage = "The transaction amount cannot greater than the remaining written off amount.";
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.greater.than.total.written.off", errorMessage);
}
this.loanStatus = statusEnum.getValue();
loanTransaction.updateLoan(this);
final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction, getLoanTransactions());
if (loanTransaction.isNotZero(loanCurrency())) {
addLoanTransaction(loanTransaction);
}
if (loanTransaction.isNotRepayment() && loanTransaction.isNotWaiver() && loanTransaction.isNotRecoveryRepayment()) {
final String errorMessage = "A transaction of type repayment or recovery repayment or waiver was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.repayment.or.waiver.or.recovery.transaction",
errorMessage);
}
final LocalDate loanTransactionDate = loanTransaction.getTransactionDate();
if (loanTransactionDate.isBefore(getDisbursementDate())) {
final String errorMessage = "The transaction date cannot be before the loan disbursement date: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage,
loanTransactionDate, getDisbursementDate());
}
if (loanTransactionDate.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The transaction date cannot be in the future.";
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, loanTransactionDate);
}
if (loanTransaction.isInterestWaiver()) {
Money totalInterestOutstandingOnLoan = getTotalInterestOutstandingOnLoan();
if (adjustedTransaction != null) {
totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount(loanCurrency()));
}
if (loanTransaction.getAmount(loanCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) {
final String errorMessage = "The amount of interest to waive cannot be greater than total interest outstanding on loan.";
throw new InvalidLoanStateTransitionException("waive.interest", "amount.exceeds.total.outstanding.interest", errorMessage,
loanTransaction.getAmount(loanCurrency()), totalInterestOutstandingOnLoan.getAmount());
}
}
if (this.loanProduct.isMultiDisburseLoan() && adjustedTransaction == null) {
BigDecimal totalDisbursed = getDisbursedAmount();
if (totalDisbursed.compareTo(this.summary.getTotalPrincipalRepaid()) < 0) {
final String errorMessage = "The transaction cannot be done before the loan disbursement: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.done.before.disbursement", errorMessage);
}
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final LoanRepaymentScheduleInstallment currentInstallment = fetchLoanRepaymentScheduleInstallment(
loanTransaction.getTransactionDate());
boolean reprocess = true;
if (!isForeclosure() && isTransactionChronologicallyLatest && adjustedTransaction == null
&& loanTransaction.getTransactionDate().isEqual(DateUtils.getLocalDateOfTenant()) && currentInstallment != null
&& currentInstallment.getTotalOutstanding(getCurrency()).isEqualTo(loanTransaction.getAmount(getCurrency()))) {
reprocess = false;
}
if (isTransactionChronologicallyLatest && adjustedTransaction == null
&& (!reprocess || !this.repaymentScheduleDetail().isInterestRecalculationEnabled()) && !isForeclosure()) {
loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), getRepaymentScheduleInstallments(),
charges());
reprocess = false;
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
reprocess = true;
} else {
final LoanRepaymentScheduleInstallment nextInstallment = fetchRepaymentScheduleInstallment(
currentInstallment.getInstallmentNumber() + 1);
if (nextInstallment != null && nextInstallment.getTotalPaidInAdvance(getCurrency()).isGreaterThanZero()) {
reprocess = true;
}
}
}
}
if (reprocess) {
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO, currentUser);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), charges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
/***
* Commented since throwing exception if external id present for one of the transactions. for this need to
* save the reversed transactions first and then new transactions.
*/
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
}
updateLoanSummaryDerivedFields();
/**
* FIXME: Vishwas, skipping post loan transaction checks for Loan recoveries
**/
if (loanTransaction.isNotRecoveryRepayment()) {
doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
}
if (this.loanProduct.isMultiDisburseLoan()) {
BigDecimal totalDisbursed = getDisbursedAmount();
if (totalDisbursed.compareTo(this.summary.getTotalPrincipalRepaid()) < 0
&& this.repaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) {
final String errorMessage = "The transaction cannot be done before the loan disbursement: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.done.before.disbursement", errorMessage);
}
}
if (changedTransactionDetail != null) {
this.loanTransactions.removeAll(changedTransactionDetail.getNewTransactionMappings().values());
}
return changedTransactionDetail;
}
private LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate dueDate) {
LoanRepaymentScheduleInstallment installment = null;
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) {
if (dueDate.equals(loanRepaymentScheduleInstallment.getDueDate())) {
installment = loanRepaymentScheduleInstallment;
break;
}
}
return installment;
}
private List<LoanTransaction> retreiveListOfIncomePostingTransactions() {
final List<LoanTransaction> incomePostTransactions = new ArrayList<>();
List<LoanTransaction> trans = getLoanTransactions();
for (final LoanTransaction transaction : trans) {
if (transaction.isNotReversed() && transaction.isIncomePosting()) {
incomePostTransactions.add(transaction);
}
}
final LoanTransactionComparator transactionComparator = new LoanTransactionComparator();
Collections.sort(incomePostTransactions, transactionComparator);
return incomePostTransactions;
}
private List<LoanTransaction> retreiveListOfTransactionsPostDisbursement() {
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
List<LoanTransaction> trans = getLoanTransactions();
for (final LoanTransaction transaction : trans) {
if (transaction.isNotReversed() && !(transaction.isDisbursement() || transaction.isNonMonetaryTransaction())) {
repaymentsOrWaivers.add(transaction);
}
}
final LoanTransactionComparator transactionComparator = new LoanTransactionComparator();
Collections.sort(repaymentsOrWaivers, transactionComparator);
return repaymentsOrWaivers;
}
public List<LoanTransaction> retreiveListOfTransactionsPostDisbursementExcludeAccruals() {
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed()
&& !(transaction.isDisbursement() || transaction.isAccrual() || transaction.isRepaymentAtDisbursement()
|| transaction.isNonMonetaryTransaction() || transaction.isIncomePosting())) {
repaymentsOrWaivers.add(transaction);
}
}
final LoanTransactionComparator transactionComparator = new LoanTransactionComparator();
Collections.sort(repaymentsOrWaivers, transactionComparator);
return repaymentsOrWaivers;
}
private List<LoanTransaction> retreiveListOfTransactionsExcludeAccruals() {
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed() && !(transaction.isAccrual() || transaction.isNonMonetaryTransaction())) {
repaymentsOrWaivers.add(transaction);
}
}
final LoanTransactionComparator transactionComparator = new LoanTransactionComparator();
Collections.sort(repaymentsOrWaivers, transactionComparator);
return repaymentsOrWaivers;
}
private List<LoanTransaction> retreiveListOfAccrualTransactions() {
final List<LoanTransaction> transactions = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed() && transaction.isAccrual()) {
transactions.add(transaction);
}
}
final LoanTransactionComparator transactionComparator = new LoanTransactionComparator();
Collections.sort(transactions, transactionComparator);
return transactions;
}
private void doPostLoanTransactionChecks(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
if (isOverPaid()) {
// FIXME - kw - update account balance to negative amount.
handleLoanOverpayment(loanLifecycleStateMachine);
} else if (this.summary.isRepaidInFull(loanCurrency())) {
handleLoanRepaymentInFull(transactionDate, loanLifecycleStateMachine);
}
}
private void handleLoanRepaymentInFull(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
boolean isAllChargesPaid = true;
for (final LoanCharge loanCharge : this.charges) {
if (loanCharge.isActive() && loanCharge.amount().compareTo(BigDecimal.ZERO) > 0
&& !(loanCharge.isPaid() || loanCharge.isWaived())) {
isAllChargesPaid = false;
break;
}
}
if (isAllChargesPaid) {
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL,
LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
this.closedOnDate = Date.from(transactionDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
this.actualMaturityDate = Date.from(transactionDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
} else if (LoanStatus.fromInt(this.loanStatus).isOverpaid()) {
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
}
processIncomeAccrualTransactionOnLoanClosure();
}
private void processIncomeAccrualTransactionOnLoanClosure() {
if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()
&& this.status().isClosedObligationsMet()) {
Date closedDate = this.getClosedOnDate();
LocalDate closedLocalDate = LocalDate.ofInstant(closedDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
reverseTransactionsOnOrAfter(retreiveListOfIncomePostingTransactions(), closedDate);
reverseTransactionsOnOrAfter(retreiveListOfAccrualTransactions(), closedDate);
HashMap<String, BigDecimal> cumulativeIncomeFromInstallments = new HashMap<>();
determineCumulativeIncomeFromInstallments(cumulativeIncomeFromInstallments);
HashMap<String, BigDecimal> cumulativeIncomeFromIncomePosting = new HashMap<>();
determineCumulativeIncomeDetails(retreiveListOfIncomePostingTransactions(), cumulativeIncomeFromIncomePosting);
BigDecimal interestToPost = cumulativeIncomeFromInstallments.get("interest")
.subtract(cumulativeIncomeFromIncomePosting.get("interest"));
BigDecimal feeToPost = cumulativeIncomeFromInstallments.get("fee").subtract(cumulativeIncomeFromIncomePosting.get("fee"));
BigDecimal penaltyToPost = cumulativeIncomeFromInstallments.get("penalty")
.subtract(cumulativeIncomeFromIncomePosting.get("penalty"));
BigDecimal amountToPost = interestToPost.add(feeToPost).add(penaltyToPost);
LoanTransaction finalIncomeTransaction = LoanTransaction.incomePosting(this, this.getOffice(), closedDate, amountToPost,
interestToPost, feeToPost, penaltyToPost, null);
addLoanTransaction(finalIncomeTransaction);
if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
List<LoanTransaction> updatedAccrualTransactions = retreiveListOfAccrualTransactions();
LocalDate lastAccruedDate = this.getDisbursementDate();
if (updatedAccrualTransactions != null && updatedAccrualTransactions.size() > 0) {
lastAccruedDate = updatedAccrualTransactions.get(updatedAccrualTransactions.size() - 1).getTransactionDate();
}
HashMap<String, Object> feeDetails = new HashMap<>();
determineFeeDetails(lastAccruedDate, closedLocalDate, feeDetails);
LoanTransaction finalAccrual = LoanTransaction.accrueTransaction(this, this.getOffice(), closedLocalDate, amountToPost,
interestToPost, feeToPost, penaltyToPost, null);
updateLoanChargesPaidBy(finalAccrual, feeDetails, null);
addLoanTransaction(finalAccrual);
}
}
updateLoanOutstandingBalaces();
}
private void determineCumulativeIncomeFromInstallments(HashMap<String, BigDecimal> cumulativeIncomeFromInstallments) {
BigDecimal interest = BigDecimal.ZERO;
BigDecimal fee = BigDecimal.ZERO;
BigDecimal penalty = BigDecimal.ZERO;
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (LoanRepaymentScheduleInstallment installment : installments) {
interest = interest.add(installment.getInterestCharged(getCurrency()).getAmount());
fee = fee.add(installment.getFeeChargesCharged(getCurrency()).getAmount());
penalty = penalty.add(installment.getPenaltyChargesCharged(getCurrency()).getAmount());
}
cumulativeIncomeFromInstallments.put("interest", interest);
cumulativeIncomeFromInstallments.put("fee", fee);
cumulativeIncomeFromInstallments.put("penalty", penalty);
}
private void determineCumulativeIncomeDetails(Collection<LoanTransaction> transactions, HashMap<String, BigDecimal> incomeDetailsMap) {
BigDecimal interest = BigDecimal.ZERO;
BigDecimal fee = BigDecimal.ZERO;
BigDecimal penalty = BigDecimal.ZERO;
for (LoanTransaction transaction : transactions) {
interest = interest.add(transaction.getInterestPortion(getCurrency()).getAmount());
fee = fee.add(transaction.getFeeChargesPortion(getCurrency()).getAmount());
penalty = penalty.add(transaction.getPenaltyChargesPortion(getCurrency()).getAmount());
}
incomeDetailsMap.put("interest", interest);
incomeDetailsMap.put("fee", fee);
incomeDetailsMap.put("penalty", penalty);
}
private void handleLoanOverpayment(final LoanLifecycleStateMachine loanLifecycleStateMachine) {
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
this.closedOnDate = null;
this.actualMaturityDate = null;
}
private boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction,
final List<LoanTransaction> loanTransactions) {
boolean isChronologicallyLatestRepaymentOrWaiver = true;
final LocalDate currentTransactionDate = loanTransaction.getTransactionDate();
for (final LoanTransaction previousTransaction : loanTransactions) {
if (!previousTransaction.isDisbursement() && previousTransaction.isNotReversed()) {
if (currentTransactionDate.isBefore(previousTransaction.getTransactionDate())
|| currentTransactionDate.isEqual(previousTransaction.getTransactionDate())) {
isChronologicallyLatestRepaymentOrWaiver = false;
break;
}
}
}
return isChronologicallyLatestRepaymentOrWaiver;
}
private boolean isAfterLatRepayment(final LoanTransaction loanTransaction, final List<LoanTransaction> loanTransactions) {
boolean isAfterLatRepayment = true;
final LocalDate currentTransactionDate = loanTransaction.getTransactionDate();
for (final LoanTransaction previousTransaction : loanTransactions) {
if (previousTransaction.isRepayment() && previousTransaction.isNotReversed()) {
if (currentTransactionDate.isBefore(previousTransaction.getTransactionDate())) {
isAfterLatRepayment = false;
break;
}
}
}
return isAfterLatRepayment;
}
private boolean isChronologicallyLatestTransaction(final LoanTransaction loanTransaction,
final List<LoanTransaction> loanTransactions) {
boolean isChronologicallyLatestRepaymentOrWaiver = true;
final LocalDate currentTransactionDate = loanTransaction.getTransactionDate();
for (final LoanTransaction previousTransaction : loanTransactions) {
if (previousTransaction.isNotReversed()) {
if (currentTransactionDate.isBefore(previousTransaction.getTransactionDate())
|| currentTransactionDate.isEqual(previousTransaction.getTransactionDate())) {
isChronologicallyLatestRepaymentOrWaiver = false;
break;
}
}
}
return isChronologicallyLatestRepaymentOrWaiver;
}
public LocalDate possibleNextRepaymentDate() {
LocalDate earliestUnpaidInstallmentDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isNotFullyPaidOff()) {
earliestUnpaidInstallmentDate = installment.getDueDate();
break;
}
}
LocalDate lastTransactionDate = null;
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isRepayment() && transaction.isNonZero()) {
lastTransactionDate = transaction.getTransactionDate();
}
}
LocalDate possibleNextRepaymentDate = earliestUnpaidInstallmentDate;
if (lastTransactionDate != null && lastTransactionDate.isAfter(earliestUnpaidInstallmentDate)) {
possibleNextRepaymentDate = lastTransactionDate;
}
return possibleNextRepaymentDate;
}
public LoanRepaymentScheduleInstallment possibleNextRepaymentInstallment() {
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = null;
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isNotFullyPaidOff()) {
loanRepaymentScheduleInstallment = installment;
break;
}
}
return loanRepaymentScheduleInstallment;
}
public LoanTransaction deriveDefaultInterestWaiverTransaction(final LocalDateTime createdDate, final AppUser currentUser) {
final Money totalInterestOutstanding = getTotalInterestOutstandingOnLoan();
Money possibleInterestToWaive = totalInterestOutstanding.copy();
LocalDate transactionDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
if (totalInterestOutstanding.isGreaterThanZero()) {
// find earliest known instance of overdue interest and default to
// that
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency());
if (scheduledRepayment.isOverdueOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))
&& scheduledRepayment.isNotFullyPaidOff() && outstandingForPeriod.isGreaterThanZero()) {
transactionDate = scheduledRepayment.getDueDate();
possibleInterestToWaive = outstandingForPeriod;
break;
}
}
}
return LoanTransaction.waiver(getOffice(), this, possibleInterestToWaive, transactionDate, possibleInterestToWaive,
possibleInterestToWaive.zero(), createdDate, currentUser);
}
public ChangedTransactionDetail adjustExistingTransaction(final LoanTransaction newTransactionDetail,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction transactionForAdjustment,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final ScheduleGeneratorDTO scheduleGeneratorDTO, final AppUser currentUser) {
HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO();
validateActivityNotBeforeLastTransactionDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, transactionForAdjustment.getTransactionDate());
validateRepaymentDateIsOnHoliday(newTransactionDetail.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(),
holidayDetailDTO.getHolidays());
validateRepaymentDateIsOnNonWorkingDay(newTransactionDetail.getTransactionDate(), holidayDetailDTO.getWorkingDays(),
holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
ChangedTransactionDetail changedTransactionDetail = null;
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
transactionForAdjustment.getTransactionDate());
if (transactionForAdjustment.isNotRepayment() && transactionForAdjustment.isNotWaiver()) {
final String errorMessage = "Only transactions of type repayment or waiver can be adjusted.";
throw new InvalidLoanTransactionTypeException("transaction", "adjustment.is.only.allowed.to.repayment.or.waiver.transaction",
errorMessage);
}
transactionForAdjustment.reverse();
transactionForAdjustment.manuallyAdjustedOrReversed();
if (isClosedWrittenOff()) {
// find write off transaction and reverse it
final LoanTransaction writeOffTransaction = findWriteOffTransaction();
writeOffTransaction.reverse();
}
if (isClosedObligationsMet() || isClosedWrittenOff() || isClosedWithOutsandingAmountMarkedForReschedule()) {
this.loanStatus = LoanStatus.ACTIVE.getValue();
}
if (newTransactionDetail.isRepayment() || newTransactionDetail.isInterestWaiver()) {
changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine,
transactionForAdjustment, scheduleGeneratorDTO, currentUser);
}
return changedTransactionDetail;
}
public ChangedTransactionDetail undoWrittenOff(final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final ScheduleGeneratorDTO scheduleGeneratorDTO, final AppUser currentUser) {
validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO);
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final LoanTransaction writeOffTransaction = findWriteOffTransaction();
writeOffTransaction.reverse();
this.loanStatus = LoanStatus.ACTIVE.getValue();
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO, currentUser);
}
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
charges());
updateLoanSummaryDerivedFields();
return changedTransactionDetail;
}
public LoanTransaction findWriteOffTransaction() {
LoanTransaction writeOff = null;
for (final LoanTransaction transaction : this.loanTransactions) {
if (!transaction.isReversed() && transaction.isWriteOff()) {
writeOff = transaction;
}
}
return writeOff;
}
private boolean isOverPaid() {
return calculateTotalOverpayment().isGreaterThanZero();
}
private Money calculateTotalOverpayment() {
Money totalPaidInRepayments = getTotalPaidInRepayments();
final MonetaryCurrency currency = loanCurrency();
Money cumulativeTotalPaidOnInstallments = Money.zero(currency);
Money cumulativeTotalWaivedOnInstallments = Money.zero(currency);
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments
.plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestPaid(currency)))
.plus(scheduledRepayment.getFeeChargesPaid(currency)).plus(scheduledRepayment.getPenaltyChargesPaid(currency));
cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments.plus(scheduledRepayment.getInterestWaived(currency));
}
for (final LoanTransaction loanTransaction : this.loanTransactions) {
if ((loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) && !loanTransaction.isReversed()) {
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency));
}
}
// if total paid in transactions doesnt match repayment schedule then
// theres an overpayment.
return totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments);
}
public Money calculateTotalRecoveredPayments() {
Money totalRecoveredPayments = getTotalRecoveredPayments();
// in case logic for reversing recovered payment is implemented handle
// subtraction from totalRecoveredPayments
return totalRecoveredPayments;
}
private MonetaryCurrency loanCurrency() {
return this.loanRepaymentScheduleDetail.getCurrency();
}
public ChangedTransactionDetail closeAsWrittenOff(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final AppUser currentUser, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
ChangedTransactionDetail changedTransactionDetail = closeDisbursements(scheduleGeneratorDTO,
loanRepaymentScheduleTransactionProcessor, currentUser);
validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING);
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING,
LoanStatus.fromInt(this.loanStatus));
LoanTransaction loanTransaction = null;
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = statusEnum.getValue();
changes.put("status", LoanEnumerations.status(this.loanStatus));
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final LocalDate writtenOffOnLocalDate = command.localDateValueOfParameterNamed("transactionDate");
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
this.closedOnDate = Date.from(writtenOffOnLocalDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
this.writtenOffOnDate = Date.from(writtenOffOnLocalDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
this.closedBy = currentUser;
changes.put("closedOnDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("writtenOffOnDate", command.stringValueOfParameterNamed("transactionDate"));
if (writtenOffOnLocalDate.isBefore(getDisbursementDate())) {
final String errorMessage = "The date on which a loan is written off cannot be before the loan disbursement date: "
+ getDisbursementDate().toString();
throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.before.submittal.date", errorMessage,
writtenOffOnLocalDate, getDisbursementDate());
}
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.WRITE_OFF_OUTSTANDING, writtenOffOnLocalDate);
if (writtenOffOnLocalDate.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan is written off cannot be in the future.";
throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.a.future.date", errorMessage, writtenOffOnLocalDate);
}
LocalDateTime createdDate = DateUtils.getLocalDateTimeOfTenant();
loanTransaction = LoanTransaction.writeoff(this, getOffice(), writtenOffOnLocalDate, txnExternalId, createdDate, currentUser);
LocalDate lastTransactionDate = getLastUserTransactionDate();
if (lastTransactionDate.isAfter(writtenOffOnLocalDate)) {
final String errorMessage = "The date of the writeoff transaction must occur on or before previous transactions.";
throw new InvalidLoanStateTransitionException("writeoff", "must.occur.on.or.after.other.transaction.dates", errorMessage,
writtenOffOnLocalDate);
}
addLoanTransaction(loanTransaction);
loanRepaymentScheduleTransactionProcessor.handleWriteOff(loanTransaction, loanCurrency(), getRepaymentScheduleInstallments());
updateLoanSummaryDerivedFields();
}
if (changedTransactionDetail == null) {
changedTransactionDetail = new ChangedTransactionDetail();
}
changedTransactionDetail.getNewTransactionMappings().put(0L, loanTransaction);
return changedTransactionDetail;
}
private ChangedTransactionDetail closeDisbursements(final ScheduleGeneratorDTO scheduleGeneratorDTO,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final AppUser currentUser) {
ChangedTransactionDetail changedTransactionDetail = null;
if (isDisbursementAllowed() && atleastOnceDisbursed()) {
this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount());
removeDisbursementDetail();
regenerateRepaymentSchedule(scheduleGeneratorDTO, currentUser);
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO, currentUser);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), charges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
addLoanTransaction(mapEntry.getValue());
}
updateLoanSummaryDerivedFields();
LoanTransaction loanTransaction = findlatestTransaction();
doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
}
return changedTransactionDetail;
}
private LoanTransaction findlatestTransaction() {
LoanTransaction transaction = null;
for (LoanTransaction loanTransaction : this.loanTransactions) {
if (!loanTransaction.isReversed()
&& (transaction == null || transaction.getTransactionDate().isBefore(loanTransaction.getTransactionDate()))) {
transaction = loanTransaction;
}
}
return transaction;
}
public ChangedTransactionDetail close(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final ScheduleGeneratorDTO scheduleGeneratorDTO, final AppUser currentUser) {
validateAccountStatus(LoanEvent.LOAN_CLOSED);
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final LocalDate closureDate = command.localDateValueOfParameterNamed("transactionDate");
final String txnExternalId = command.stringValueOfParameterNamedAllowingNull("externalId");
this.closedOnDate = Date.from(closureDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
changes.put("closedOnDate", command.stringValueOfParameterNamed("transactionDate"));
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.REPAID_IN_FULL, closureDate);
if (closureDate.isBefore(getDisbursementDate())) {
final String errorMessage = "The date on which a loan is closed cannot be before the loan disbursement date: "
+ getDisbursementDate().toString();
throw new InvalidLoanStateTransitionException("close", "cannot.be.before.submittal.date", errorMessage, closureDate,
getDisbursementDate());
}
if (closureDate.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan is closed cannot be in the future.";
throw new InvalidLoanStateTransitionException("close", "cannot.be.a.future.date", errorMessage, closureDate);
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
ChangedTransactionDetail changedTransactionDetail = closeDisbursements(scheduleGeneratorDTO,
loanRepaymentScheduleTransactionProcessor, currentUser);
LoanTransaction loanTransaction = null;
if (isOpen()) {
final Money totalOutstanding = this.summary.getTotalOutstanding(loanCurrency());
if (totalOutstanding.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(totalOutstanding)) {
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL,
LoanStatus.fromInt(this.loanStatus));
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = statusEnum.getValue();
changes.put("status", LoanEnumerations.status(this.loanStatus));
}
this.closedOnDate = Date.from(closureDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
loanTransaction = LoanTransaction.writeoff(this, getOffice(), closureDate, txnExternalId,
DateUtils.getLocalDateTimeOfTenant(), currentUser);
final boolean isLastTransaction = isChronologicallyLatestTransaction(loanTransaction, getLoanTransactions());
if (!isLastTransaction) {
final String errorMessage = "The closing date of the loan must be on or after latest transaction date.";
throw new InvalidLoanStateTransitionException("close.loan", "must.occur.on.or.after.latest.transaction.date",
errorMessage, closureDate);
}
addLoanTransaction(loanTransaction);
loanRepaymentScheduleTransactionProcessor.handleWriteOff(loanTransaction, loanCurrency(),
getRepaymentScheduleInstallments());
updateLoanSummaryDerivedFields();
} else if (totalOutstanding.isGreaterThanZero()) {
final String errorMessage = "A loan with money outstanding cannot be closed";
throw new InvalidLoanStateTransitionException("close", "loan.has.money.outstanding", errorMessage,
totalOutstanding.toString());
}
}
if (isOverPaid()) {
final Money totalLoanOverpayment = calculateTotalOverpayment();
if (totalLoanOverpayment.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(totalLoanOverpayment)) {
// TODO - KW - technically should set somewhere that this loan
// has
// 'overpaid' amount
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL,
LoanStatus.fromInt(this.loanStatus));
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = statusEnum.getValue();
changes.put("status", LoanEnumerations.status(this.loanStatus));
}
this.closedOnDate = Date.from(closureDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
} else if (totalLoanOverpayment.isGreaterThanZero()) {
final String errorMessage = "The loan is marked as 'Overpaid' and cannot be moved to 'Closed (obligations met).";
throw new InvalidLoanStateTransitionException("close", "loan.is.overpaid", errorMessage, totalLoanOverpayment.toString());
}
}
if (changedTransactionDetail == null) {
changedTransactionDetail = new ChangedTransactionDetail();
}
changedTransactionDetail.getNewTransactionMappings().put(0L, loanTransaction);
return changedTransactionDetail;
}
/**
* Behaviour added to comply with capability of previous mifos product to support easier transition to fineract
* platform.
*/
public void closeAsMarkedForReschedule(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final Map<String, Object> changes) {
final LocalDate rescheduledOn = command.localDateValueOfParameterNamed("transactionDate");
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_RESCHEDULE, LoanStatus.fromInt(this.loanStatus));
if (!statusEnum.hasStateOf(LoanStatus.fromInt(this.loanStatus))) {
this.loanStatus = statusEnum.getValue();
changes.put("status", LoanEnumerations.status(this.loanStatus));
}
this.closedOnDate = Date.from(rescheduledOn.atStartOfDay(ZoneId.systemDefault()).toInstant());
this.rescheduledOnDate = Date.from(rescheduledOn.atStartOfDay(ZoneId.systemDefault()).toInstant());
changes.put("closedOnDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("rescheduledOnDate", command.stringValueOfParameterNamed("transactionDate"));
final LocalDate rescheduledOnLocalDate = LocalDate.ofInstant(this.rescheduledOnDate.toInstant(),
DateUtils.getDateTimeZoneOfTenant());
if (rescheduledOnLocalDate.isBefore(getDisbursementDate())) {
final String errorMessage = "The date on which a loan is rescheduled cannot be before the loan disbursement date: "
+ getDisbursementDate().toString();
throw new InvalidLoanStateTransitionException("close.reschedule", "cannot.be.before.submittal.date", errorMessage,
rescheduledOnLocalDate, getDisbursementDate());
}
if (rescheduledOnLocalDate.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The date on which a loan is rescheduled cannot be in the future.";
throw new InvalidLoanStateTransitionException("close.reschedule", "cannot.be.a.future.date", errorMessage,
rescheduledOnLocalDate);
}
}
public boolean isNotSubmittedAndPendingApproval() {
return !isSubmittedAndPendingApproval();
}
public LoanStatus status() {
return LoanStatus.fromInt(this.loanStatus);
}
public boolean isSubmittedAndPendingApproval() {
return status().isSubmittedAndPendingApproval();
}
public boolean isApproved() {
return status().isApproved();
}
private boolean isNotDisbursed() {
return !isDisbursed();
}
public boolean isChargesAdditionAllowed() {
boolean isDisbursed = false;
if (this.loanProduct.isMultiDisburseLoan()) {
isDisbursed = !isDisbursementAllowed();
} else {
isDisbursed = hasDisbursementTransaction();
}
return isDisbursed;
}
public boolean isDisbursed() {
return hasDisbursementTransaction();
}
public boolean isClosed() {
return status().isClosed() || isCancelled();
}
private boolean isClosedObligationsMet() {
return status().isClosedObligationsMet();
}
public boolean isClosedWrittenOff() {
return status().isClosedWrittenOff();
}
private boolean isClosedWithOutsandingAmountMarkedForReschedule() {
return status().isClosedWithOutsandingAmountMarkedForReschedule();
}
private boolean isCancelled() {
return isRejected() || isWithdrawn();
}
private boolean isWithdrawn() {
return status().isWithdrawnByClient();
}
private boolean isRejected() {
return status().isRejected();
}
public boolean isOpen() {
return status().isActive();
}
private boolean isAllTranchesNotDisbursed() {
return this.loanProduct.isMultiDisburseLoan()
&& (LoanStatus.fromInt(this.loanStatus).isActive() || LoanStatus.fromInt(this.loanStatus).isApproved())
&& isDisbursementAllowed();
}
private boolean hasDisbursementTransaction() {
boolean hasRepaymentTransaction = false;
for (final LoanTransaction loanTransaction : this.loanTransactions) {
if (loanTransaction.isDisbursement() && loanTransaction.isNotReversed()) {
hasRepaymentTransaction = true;
break;
}
}
return hasRepaymentTransaction;
}
public boolean isSubmittedOnDateAfter(final LocalDate compareDate) {
return this.submittedOnDate == null ? false
: LocalDate.ofInstant(this.submittedOnDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()).isAfter(compareDate);
}
public LocalDate getSubmittedOnDate() {
return ObjectUtils.defaultIfNull(LocalDate.ofInstant(this.submittedOnDate.toInstant(), DateUtils.getDateTimeZoneOfTenant()), null);
}
public LocalDate getApprovedOnDate() {
LocalDate date = null;
if (this.approvedOnDate != null) {
date = LocalDate.ofInstant(this.approvedOnDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
return date;
}
public LocalDate getExpectedDisbursedOnLocalDate() {
LocalDate expectedDisbursementDate = null;
if (this.expectedDisbursementDate != null) {
expectedDisbursementDate = LocalDate.ofInstant(this.expectedDisbursementDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
return expectedDisbursementDate;
}
public LocalDate getDisbursementDate() {
LocalDate disbursementDate = getExpectedDisbursedOnLocalDate();
if (this.actualDisbursementDate != null) {
disbursementDate = LocalDate.ofInstant(this.actualDisbursementDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
return disbursementDate;
}
public void setActualDisbursementDate(Date actualDisbursementDate) {
this.actualDisbursementDate = actualDisbursementDate;
}
public LocalDate getWrittenOffDate() {
LocalDate writtenOffDate = null;
if (this.writtenOffOnDate != null) {
writtenOffDate = LocalDate.ofInstant(this.writtenOffOnDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
return writtenOffDate;
}
public LocalDate getExpectedDisbursedOnLocalDateForTemplate() {
LocalDate expectedDisbursementDate = null;
if (this.expectedDisbursementDate != null) {
expectedDisbursementDate = LocalDate.ofInstant(this.expectedDisbursementDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
Collection<LoanDisbursementDetails> details = fetchUndisbursedDetail();
if (!details.isEmpty()) {
for (LoanDisbursementDetails disbursementDetails : details) {
expectedDisbursementDate = LocalDate.ofInstant(disbursementDetails.expectedDisbursementDate().toInstant(),
DateUtils.getDateTimeZoneOfTenant());
}
}
return expectedDisbursementDate;
}
/*
* Reason for derving
*/
public BigDecimal getDisburseAmountForTemplate() {
BigDecimal principal = this.loanRepaymentScheduleDetail.getPrincipal().getAmount();
Collection<LoanDisbursementDetails> details = fetchUndisbursedDetail();
if (!details.isEmpty()) {
principal = BigDecimal.ZERO;
for (LoanDisbursementDetails disbursementDetails : details) {
principal = principal.add(disbursementDetails.principal());
}
}
return principal;
}
public LocalDate getExpectedFirstRepaymentOnDate() {
LocalDate firstRepaymentDate = null;
if (this.expectedFirstRepaymentOnDate != null) {
firstRepaymentDate = LocalDate.ofInstant(this.expectedFirstRepaymentOnDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
return firstRepaymentDate;
}
private boolean isActualDisbursedOnDateEarlierOrLaterThanExpected(final LocalDate actualDisbursedOnDate) {
boolean isRegenerationRequired = false;
if (this.loanProduct.isMultiDisburseLoan()) {
LoanDisbursementDetails details = fetchLastDisburseDetail();
if (details != null && details.expectedDisbursementDate().compareTo(details.actualDisbursementDate()) == 0 ? Boolean.FALSE
: Boolean.TRUE) {
isRegenerationRequired = true;
}
}
return !LocalDate.ofInstant(this.expectedDisbursementDate.toInstant(), DateUtils.getDateTimeZoneOfTenant())
.isEqual(actualDisbursedOnDate) || isRegenerationRequired;
}
private boolean isRepaymentScheduleRegenerationRequiredForDisbursement(final LocalDate actualDisbursementDate) {
return isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate);
}
private Money getTotalPaidInRepayments() {
Money cumulativePaid = Money.zero(loanCurrency());
for (final LoanTransaction repayment : this.loanTransactions) {
if (repayment.isRepayment() && !repayment.isReversed()) {
cumulativePaid = cumulativePaid.plus(repayment.getAmount(loanCurrency()));
}
}
return cumulativePaid;
}
public Money getTotalRecoveredPayments() {
Money cumulativePaid = Money.zero(loanCurrency());
for (final LoanTransaction recoveredPayment : this.loanTransactions) {
if (recoveredPayment.isRecoveryRepayment()) {
cumulativePaid = cumulativePaid.plus(recoveredPayment.getAmount(loanCurrency()));
}
}
return cumulativePaid;
}
private Money getTotalInterestOutstandingOnLoan() {
Money cumulativeInterest = Money.zero(loanCurrency());
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment.getInterestOutstanding(loanCurrency()));
}
return cumulativeInterest;
}
@SuppressWarnings("unused")
private Money getTotalInterestOverdueOnLoan() {
Money cumulativeInterestOverdue = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency());
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
final Money interestOutstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency());
if (scheduledRepayment.isOverdueOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()))) {
cumulativeInterestOverdue = cumulativeInterestOverdue.plus(interestOutstandingForPeriod);
}
}
return cumulativeInterestOverdue;
}
private Money getInArrearsTolerance() {
return this.loanRepaymentScheduleDetail.getInArrearsTolerance();
}
public boolean hasIdentifyOf(final Long loanId) {
return loanId.equals(getId());
}
public boolean hasLoanOfficer(final Staff fromLoanOfficer) {
boolean matchesCurrentLoanOfficer = false;
if (this.loanOfficer != null) {
matchesCurrentLoanOfficer = this.loanOfficer.identifiedBy(fromLoanOfficer);
} else {
matchesCurrentLoanOfficer = fromLoanOfficer == null;
}
return matchesCurrentLoanOfficer;
}
public LocalDate getInterestChargedFromDate() {
LocalDate interestChargedFrom = null;
if (this.interestChargedFromDate != null) {
interestChargedFrom = LocalDate.ofInstant(this.interestChargedFromDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
return interestChargedFrom;
}
public Money getPrincpal() {
return this.loanRepaymentScheduleDetail.getPrincipal();
}
public boolean hasCurrencyCodeOf(final String matchingCurrencyCode) {
return getCurrencyCode().equalsIgnoreCase(matchingCurrencyCode);
}
public String getCurrencyCode() {
return this.loanRepaymentScheduleDetail.getPrincipal().getCurrencyCode();
}
public MonetaryCurrency getCurrency() {
return this.loanRepaymentScheduleDetail.getCurrency();
}
public void reassignLoanOfficer(final Staff newLoanOfficer, final LocalDate assignmentDate) {
final LoanOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord();
final LoanOfficerAssignmentHistory lastAssignmentRecord = findLastAssignmentHistoryRecord(newLoanOfficer);
// assignment date should not be less than loan submitted date
if (isSubmittedOnDateAfter(assignmentDate)) {
final String errorMessage = "The Loan Officer assignment date (" + assignmentDate.toString()
+ ") cannot be before loan submitted date (" + getSubmittedOnDate().toString() + ").";
throw new LoanOfficerAssignmentDateException("cannot.be.before.loan.submittal.date", errorMessage, assignmentDate,
getSubmittedOnDate());
} else if (lastAssignmentRecord != null && lastAssignmentRecord.isEndDateAfter(assignmentDate)) {
final String errorMessage = "The Loan Officer assignment date (" + assignmentDate
+ ") cannot be before previous Loan Officer unassigned date (" + lastAssignmentRecord.getEndDate() + ").";
throw new LoanOfficerAssignmentDateException("cannot.be.before.previous.unassignement.date", errorMessage, assignmentDate,
lastAssignmentRecord.getEndDate());
} else if (DateUtils.getLocalDateOfTenant().isBefore(assignmentDate)) {
final String errorMessage = "The Loan Officer assignment date (" + assignmentDate + ") cannot be in the future.";
throw new LoanOfficerAssignmentDateException("cannot.be.a.future.date", errorMessage, assignmentDate);
} else if (latestHistoryRecord != null && this.loanOfficer.identifiedBy(newLoanOfficer)) {
latestHistoryRecord.updateStartDate(assignmentDate);
} else if (latestHistoryRecord != null && latestHistoryRecord.matchesStartDateOf(assignmentDate)) {
latestHistoryRecord.updateLoanOfficer(newLoanOfficer);
this.loanOfficer = newLoanOfficer;
} else if (latestHistoryRecord != null && latestHistoryRecord.hasStartDateBefore(assignmentDate)) {
final String errorMessage = "Loan with identifier " + getId() + " was already assigned before date " + assignmentDate;
throw new LoanOfficerAssignmentDateException("is.before.last.assignment.date", errorMessage, getId(), assignmentDate);
} else {
if (latestHistoryRecord != null) {
// loan officer correctly changed from previous loan officer to
// new loan officer
latestHistoryRecord.updateEndDate(assignmentDate);
}
this.loanOfficer = newLoanOfficer;
if (isNotSubmittedAndPendingApproval()) {
final LoanOfficerAssignmentHistory loanOfficerAssignmentHistory = LoanOfficerAssignmentHistory.createNew(this,
this.loanOfficer, assignmentDate);
this.loanOfficerHistory.add(loanOfficerAssignmentHistory);
}
}
}
public void removeLoanOfficer(final LocalDate unassignDate) {
final LoanOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord();
if (latestHistoryRecord != null) {
validateUnassignDate(latestHistoryRecord, unassignDate);
latestHistoryRecord.updateEndDate(unassignDate);
}
this.loanOfficer = null;
}
private void validateUnassignDate(final LoanOfficerAssignmentHistory latestHistoryRecord, final LocalDate unassignDate) {
final LocalDate today = DateUtils.getLocalDateOfTenant();
if (latestHistoryRecord.getStartDate().isAfter(unassignDate)) {
final String errorMessage = "The Loan officer Unassign date(" + unassignDate + ") cannot be before its assignment date ("
+ latestHistoryRecord.getStartDate() + ").";
throw new LoanOfficerUnassignmentDateException("cannot.be.before.assignment.date", errorMessage, getId(),
getLoanOfficer().getId(), latestHistoryRecord.getStartDate(), unassignDate);
} else if (unassignDate.isAfter(today)) {
final String errorMessage = "The Loan Officer Unassign date (" + unassignDate + ") cannot be in the future.";
throw new LoanOfficerUnassignmentDateException("cannot.be.a.future.date", errorMessage, unassignDate);
}
}
private LoanOfficerAssignmentHistory findLatestIncompleteHistoryRecord() {
LoanOfficerAssignmentHistory latestRecordWithNoEndDate = null;
for (final LoanOfficerAssignmentHistory historyRecord : this.loanOfficerHistory) {
if (historyRecord.isCurrentRecord()) {
latestRecordWithNoEndDate = historyRecord;
break;
}
}
return latestRecordWithNoEndDate;
}
private LoanOfficerAssignmentHistory findLastAssignmentHistoryRecord(final Staff newLoanOfficer) {
LoanOfficerAssignmentHistory lastAssignmentRecordLatestEndDate = null;
for (final LoanOfficerAssignmentHistory historyRecord : this.loanOfficerHistory) {
if (historyRecord.isCurrentRecord() && !historyRecord.isSameLoanOfficer(newLoanOfficer)) {
lastAssignmentRecordLatestEndDate = historyRecord;
break;
}
if (lastAssignmentRecordLatestEndDate == null) {
lastAssignmentRecordLatestEndDate = historyRecord;
} else if (historyRecord.isEndDateAfter(lastAssignmentRecordLatestEndDate.getEndDate())
&& !historyRecord.isSameLoanOfficer(newLoanOfficer)) {
lastAssignmentRecordLatestEndDate = historyRecord;
}
}
return lastAssignmentRecordLatestEndDate;
}
public Long getClientId() {
Long clientId = null;
if (this.client != null) {
clientId = this.client.getId();
}
return clientId;
}
public Long getGroupId() {
Long groupId = null;
if (this.group != null) {
groupId = this.group.getId();
}
return groupId;
}
public Long getGlimId() {
Long glimId = null;
if (this.glim != null) {
glimId = this.glim.getId();
}
return glimId;
}
public Long getOfficeId() {
Long officeId = null;
if (this.client != null) {
officeId = this.client.officeId();
} else {
officeId = this.group.officeId();
}
return officeId;
}
public Office getOffice() {
Office office = null;
if (this.client != null) {
office = this.client.getOffice();
} else {
office = this.group.getOffice();
}
return office;
}
private Boolean isCashBasedAccountingEnabledOnLoanProduct() {
return this.loanProduct.isCashBasedAccountingEnabled();
}
public Boolean isUpfrontAccrualAccountingEnabledOnLoanProduct() {
return this.loanProduct.isUpfrontAccrualAccountingEnabled();
}
public Boolean isAccountingDisabledOnLoanProduct() {
return this.loanProduct.isAccountingDisabled();
}
public Boolean isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct() {
return isCashBasedAccountingEnabledOnLoanProduct() || isUpfrontAccrualAccountingEnabledOnLoanProduct()
|| isAccountingDisabledOnLoanProduct();
}
public Boolean isPeriodicAccrualAccountingEnabledOnLoanProduct() {
return this.loanProduct.isPeriodicAccrualAccountingEnabled();
}
public Long productId() {
return this.loanProduct.getId();
}
public Staff getLoanOfficer() {
return this.loanOfficer;
}
public LoanSummary getSummary() {
return this.summary;
}
public Set<LoanCollateral> getCollateral() {
return this.collateral;
}
public BigDecimal getProposedPrincipal() {
return this.proposedPrincipal;
}
public Map<String, Object> deriveAccountingBridgeData(final CurrencyData currencyData, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, boolean isAccountTransfer) {
final Map<String, Object> accountingBridgeData = new LinkedHashMap<>();
accountingBridgeData.put("loanId", getId());
accountingBridgeData.put("loanProductId", productId());
accountingBridgeData.put("officeId", getOfficeId());
accountingBridgeData.put("currency", currencyData);
accountingBridgeData.put("calculatedInterest", this.summary.getTotalInterestCharged());
accountingBridgeData.put("cashBasedAccountingEnabled", isCashBasedAccountingEnabledOnLoanProduct());
accountingBridgeData.put("upfrontAccrualBasedAccountingEnabled", isUpfrontAccrualAccountingEnabledOnLoanProduct());
accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", isPeriodicAccrualAccountingEnabledOnLoanProduct());
accountingBridgeData.put("isAccountTransfer", isAccountTransfer);
final List<Map<String, Object>> newLoanTransactions = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isReversed() && existingTransactionIds.contains(transaction.getId())
&& !existingReversedTransactionIds.contains(transaction.getId())) {
newLoanTransactions.add(transaction.toMapData(currencyData));
} else if (!existingTransactionIds.contains(transaction.getId())) {
newLoanTransactions.add(transaction.toMapData(currencyData));
}
}
accountingBridgeData.put("newLoanTransactions", newLoanTransactions);
return accountingBridgeData;
}
public Money getReceivableInterest(final LocalDate tillDate) {
Money receivableInterest = Money.zero(getCurrency());
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement()
&& !transaction.getTransactionDate().isAfter(tillDate)) {
if (transaction.isAccrual()) {
receivableInterest = receivableInterest.plus(transaction.getInterestPortion(getCurrency()));
} else if (transaction.isRepayment() || transaction.isInterestWaiver()) {
receivableInterest = receivableInterest.minus(transaction.getInterestPortion(getCurrency()));
}
}
if (receivableInterest.isLessThanZero()) {
receivableInterest = receivableInterest.zero();
}
/*
* if (transaction.getTransactionDate().isAfter(tillDate) && transaction.isAccrual()) { final String
* errorMessage = "The date on which a loan is interest waived cannot be in after accrual transactions." ;
* throw new InvalidLoanStateTransitionException("waive", "cannot.be.after.accrual.date", errorMessage,
* tillDate); }
*/
}
return receivableInterest;
}
public void setHelpers(final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanSummaryWrapper loanSummaryWrapper,
final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory) {
this.loanLifecycleStateMachine = loanLifecycleStateMachine;
this.loanSummaryWrapper = loanSummaryWrapper;
this.transactionProcessorFactory = transactionProcessorFactory;
}
public boolean isSyncDisbursementWithMeeting() {
return this.syncDisbursementWithMeeting == null ? false : this.syncDisbursementWithMeeting;
}
public Date getClosedOnDate() {
return this.closedOnDate;
}
public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays,
final Boolean reschedulebasedOnMeetingDates, final LocalDate presentMeetingDate, final LocalDate newMeetingDate,
final boolean isSkipRepaymentonfirstdayofmonth, final Integer numberofDays) {
// first repayment's from date is same as disbursement date.
/*
* meetingStartDate is used as seedDate Capture the seedDate from user and use the seedDate as meetingStart date
*/
LocalDate tmpFromDate = getDisbursementDate();
final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType();
final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery();
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
LocalDate newRepaymentDate = null;
Boolean isFirstTime = true;
LocalDate latestRepaymentDate = null;
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) {
LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
if (oldDueDate.isEqual(presentMeetingDate) || oldDueDate.isAfter(presentMeetingDate)) {
if (isFirstTime) {
isFirstTime = false;
newRepaymentDate = newMeetingDate;
} else {
// tmpFromDate.plusDays(1) is done to make sure
// getNewRepaymentMeetingDate method returns next meeting
// date and not the same as tmpFromDate
newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, tmpFromDate, tmpFromDate.plusDays(1),
loanRepaymentInterval, frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays);
}
if (isHolidayEnabled) {
newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays);
}
if (latestRepaymentDate == null || latestRepaymentDate.isBefore(newRepaymentDate)) {
latestRepaymentDate = newRepaymentDate;
}
loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate);
// reset from date to get actual daysInPeriod
if (!isFirstTime) {
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
}
tmpFromDate = newRepaymentDate;// update with new repayment
// date
} else {
tmpFromDate = oldDueDate;
}
}
if (latestRepaymentDate != null) {
this.expectedMaturityDate = Date.from(latestRepaymentDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
}
}
public void updateLoanRepaymentScheduleDates(final LocalDate meetingStartDate, final String recuringRule,
final boolean isHolidayEnabled, final List<Holiday> holidays, final WorkingDays workingDays,
final boolean isSkipRepaymentonfirstdayofmonth, final Integer numberofDays) {
// first repayment's from date is same as disbursement date.
LocalDate tmpFromDate = getDisbursementDate();
final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType();
final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery();
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
LocalDate newRepaymentDate = null;
LocalDate seedDate = meetingStartDate;
LocalDate latestRepaymentDate = null;
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) {
LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
// FIXME: AA this won't update repayment dates before current date.
if (oldDueDate.isAfter(seedDate) && oldDueDate.isAfter(DateUtils.getLocalDateOfTenant())) {
newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, seedDate, oldDueDate, loanRepaymentInterval,
frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays);
final LocalDate maxDateLimitForNewRepayment = getMaxDateLimitForNewRepayment(repaymentPeriodFrequencyType,
loanRepaymentInterval, tmpFromDate);
if (newRepaymentDate.isAfter(maxDateLimitForNewRepayment)) {
newRepaymentDate = CalendarUtils.getNextRepaymentMeetingDate(recuringRule, seedDate, tmpFromDate, loanRepaymentInterval,
frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays);
}
if (isHolidayEnabled) {
newRepaymentDate = HolidayUtil.getRepaymentRescheduleDateToIfHoliday(newRepaymentDate, holidays);
}
if (latestRepaymentDate == null || latestRepaymentDate.isBefore(newRepaymentDate)) {
latestRepaymentDate = newRepaymentDate;
}
loanRepaymentScheduleInstallment.updateDueDate(newRepaymentDate);
// reset from date to get actual daysInPeriod
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
tmpFromDate = newRepaymentDate;// update with new repayment
// date
} else {
tmpFromDate = oldDueDate;
}
}
if (latestRepaymentDate != null) {
this.expectedMaturityDate = Date.from(latestRepaymentDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
}
}
private LocalDate getMaxDateLimitForNewRepayment(final PeriodFrequencyType periodFrequencyType, final Integer loanRepaymentInterval,
final LocalDate startDate) {
LocalDate dueRepaymentPeriodDate = startDate;
final Integer repaidEvery = 2 * loanRepaymentInterval;
switch (periodFrequencyType) {
case DAYS:
dueRepaymentPeriodDate = startDate.plusDays(repaidEvery);
break;
case WEEKS:
dueRepaymentPeriodDate = startDate.plusWeeks(repaidEvery);
break;
case MONTHS:
dueRepaymentPeriodDate = startDate.plusMonths(repaidEvery);
break;
case YEARS:
dueRepaymentPeriodDate = startDate.plusYears(repaidEvery);
break;
case INVALID:
break;
case WHOLE_TERM:
break;
}
return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from
// startDate
}
public void applyHolidayToRepaymentScheduleDates(final Holiday holiday, final LoanUtilService loanUtilService) {
final DefaultScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(this, holiday.getFromDateLocalDate());
final LoanApplicationTerms loanApplicationTerms = this.constructLoanApplicationTerms(scheduleGeneratorDTO);
// LocalDate rescheduleFromDate = holiday.getFromDateLocalDate();
LocalDate adjustedRescheduleToDate = null;
if (holiday.getReScheduleType().isResheduleToNextRepaymentDate()) {
final LocalDate rescheduleToDate = holiday.getToDateLocalDate();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : this.getRepaymentScheduleInstallments()) {
if (rescheduleToDate.isEqual(loanRepaymentScheduleInstallment.getDueDate())) {
adjustedRescheduleToDate = rescheduleToDate;
break;
} else {
// Account for a Bimonthly Loan Product
// if (loanApplicationTerms.getRepaymentPeriodFrequencyType().isSemiMonthly()) {
// if (rescheduleToDate.isAfter(loanRepaymentScheduleInstallment.getDueDate())
// && rescheduleToDate.isBefore(loanRepaymentScheduleInstallment.getDueDate().plusDays(15))) {
// adjustedRescheduleToDate = loanRepaymentScheduleInstallment.getDueDate();
// break;
// }
// } else {
// // Once Bimonthly product implemented, Replace the `Standard Monthly Loan Holiday check` below
// HERE
// if (rescheduleToDate.isAfter(loanRepaymentScheduleInstallment.getDueDate())
// && rescheduleToDate.isBefore(loanRepaymentScheduleInstallment.getDueDate().plusDays(30))) {
// adjustedRescheduleToDate = loanRepaymentScheduleInstallment.getDueDate();
// break;
// }
// }
// Standard Monthly Loan Holiday check
if (rescheduleToDate.isAfter(loanRepaymentScheduleInstallment.getDueDate())
&& rescheduleToDate.isBefore(loanRepaymentScheduleInstallment.getDueDate().plusDays(30))) {
adjustedRescheduleToDate = loanRepaymentScheduleInstallment.getDueDate();
break;
}
}
}
} else {
adjustedRescheduleToDate = holiday.getRepaymentsRescheduledToLocalDate();
}
// first repayment's from date is same as disbursement date.
LocalDate tmpFromDate = getDisbursementDate();
// Loop through all loanRepayments
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : installments) {
if (adjustedRescheduleToDate == null) {
break;
}
final LocalDate oldDueDate = loanRepaymentScheduleInstallment.getDueDate();
// update from date if it's not same as previous installament's due
// date.
if (!loanRepaymentScheduleInstallment.getFromDate().isEqual(tmpFromDate)) {
loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate);
}
if (oldDueDate.equals(holiday.getFromDateLocalDate()) || oldDueDate.isAfter(holiday.getFromDateLocalDate())) {
// FIXME: AA do we need to apply non-working days.
// Assuming holiday's repayment reschedule to date cannot be
// created on a non-working day.
adjustedRescheduleToDate = scheduledDateGenerator.generateNextRepaymentDate(adjustedRescheduleToDate, loanApplicationTerms,
false);
loanRepaymentScheduleInstallment.updateDueDate(adjustedRescheduleToDate);
}
tmpFromDate = loanRepaymentScheduleInstallment.getDueDate();
}
}
private void validateDisbursementDateIsOnNonWorkingDay(final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) {
if (!allowTransactionsOnNonWorkingDay) {
if (!WorkingDaysUtil.isWorkingDay(workingDays, getDisbursementDate())) {
final String errorMessage = "Expected disbursement date cannot be on a non working day";
throw new LoanApplicationDateException("disbursement.date.on.non.working.day", errorMessage,
getExpectedDisbursedOnLocalDate());
}
}
}
private void validateDisbursementDateIsOnHoliday(final boolean allowTransactionsOnHoliday, final List<Holiday> holidays) {
if (!allowTransactionsOnHoliday) {
if (HolidayUtil.isHoliday(getDisbursementDate(), holidays)) {
final String errorMessage = "Expected disbursement date cannot be on a holiday";
throw new LoanApplicationDateException("disbursement.date.on.holiday", errorMessage, getExpectedDisbursedOnLocalDate());
}
}
}
public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays,
final boolean allowTransactionsOnNonWorkingDay) {
if (!allowTransactionsOnNonWorkingDay) {
if (!WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) {
final String errorMessage = "Repayment date cannot be on a non working day";
throw new LoanApplicationDateException("repayment.date.on.non.working.day", errorMessage, repaymentDate);
}
}
}
public void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean allowTransactionsOnHoliday,
final List<Holiday> holidays) {
if (!allowTransactionsOnHoliday) {
if (HolidayUtil.isHoliday(repaymentDate, holidays)) {
final String errorMessage = "Repayment date cannot be on a holiday";
throw new LoanApplicationDateException("repayment.date.on.holiday", errorMessage, repaymentDate);
}
}
}
public Group group() {
return this.group;
}
public void updateGroup(final Group newGroup) {
this.group = newGroup;
}
public Integer getCurrentLoanCounter() {
return this.loanCounter;
}
public Integer getLoanProductLoanCounter() {
if (this.loanProductCounter == null) {
return 0;
}
return this.loanProductCounter;
}
public void updateClientLoanCounter(final Integer newLoanCounter) {
this.loanCounter = newLoanCounter;
}
public void updateLoanProductLoanCounter(final Integer newLoanProductLoanCounter) {
this.loanProductCounter = newLoanProductLoanCounter;
}
public boolean isGroupLoan() {
return AccountType.fromInt(this.loanType).isGroupAccount();
}
public boolean isJLGLoan() {
return AccountType.fromInt(this.loanType).isJLGAccount();
}
public void updateInterestRateFrequencyType() {
this.loanRepaymentScheduleDetail.updatenterestPeriodFrequencyType(this.loanProduct.getInterestPeriodFrequencyType());
}
public Integer getTermFrequency() {
return this.termFrequency;
}
public Integer getTermPeriodFrequencyType() {
return this.termPeriodFrequencyType;
}
// This method returns copy of all transactions
public List<LoanTransaction> getLoanTransactions() {
return this.loanTransactions;
}
public void addLoanTransaction(final LoanTransaction loanTransaction) {
this.loanTransactions.add(loanTransaction);
}
public void removeLoanTransaction(final LoanTransaction loanTransaction) {
this.loanTransactions.remove(loanTransaction);
}
public void setLoanStatus(final Integer loanStatus) {
this.loanStatus = loanStatus;
}
public void validateExpectedDisbursementForHolidayAndNonWorkingDay(final WorkingDays workingDays,
final boolean allowTransactionsOnHoliday, final List<Holiday> holidays, final boolean allowTransactionsOnNonWorkingDay) {
// validate if disbursement date is a holiday or a non-working day
validateDisbursementDateIsOnNonWorkingDay(workingDays, allowTransactionsOnNonWorkingDay);
validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays);
}
private void validateActivityNotBeforeClientOrGroupTransferDate(final LoanEvent event, final LocalDate activityDate) {
if (this.client != null && this.client.getOfficeJoiningLocalDate() != null) {
final LocalDate clientOfficeJoiningDate = this.client.getOfficeJoiningLocalDate();
if (activityDate.isBefore(clientOfficeJoiningDate)) {
String errorMessage = null;
String action = null;
String postfix = null;
switch (event) {
case LOAN_CREATED:
errorMessage = "The date on which a loan is submitted cannot be earlier than client's transfer date to this office";
action = "submittal";
postfix = "cannot.be.before.client.transfer.date";
break;
case LOAN_APPROVED:
errorMessage = "The date on which a loan is approved cannot be earlier than client's transfer date to this office";
action = "approval";
postfix = "cannot.be.before.client.transfer.date";
break;
case LOAN_APPROVAL_UNDO:
errorMessage = "The date on which a loan is approved cannot be earlier than client's transfer date to this office";
action = "approval";
postfix = "cannot.be.undone.before.client.transfer.date";
break;
case LOAN_DISBURSED:
errorMessage = "The date on which a loan is disbursed cannot be earlier than client's transfer date to this office";
action = "disbursal";
postfix = "cannot.be.before.client.transfer.date";
break;
case LOAN_DISBURSAL_UNDO:
errorMessage = "Cannot undo a disbursal done in another branch";
action = "disbursal";
postfix = "cannot.be.undone.before.client.transfer.date";
break;
case LOAN_REPAYMENT_OR_WAIVER:
errorMessage = "The date on which a repayment or waiver is made cannot be earlier than client's transfer date to this office";
action = "repayment.or.waiver";
postfix = "cannot.be.made.before.client.transfer.date";
break;
case LOAN_REJECTED:
errorMessage = "The date on which a loan is rejected cannot be earlier than client's transfer date to this office";
action = "reject";
postfix = "cannot.be.before.client.transfer.date";
break;
case LOAN_WITHDRAWN:
errorMessage = "The date on which a loan is withdrawn cannot be earlier than client's transfer date to this office";
action = "withdraw";
postfix = "cannot.be.before.client.transfer.date";
break;
case WRITE_OFF_OUTSTANDING:
errorMessage = "The date on which a write off is made cannot be earlier than client's transfer date to this office";
action = "writeoff";
postfix = "cannot.be.undone.before.client.transfer.date";
break;
case REPAID_IN_FULL:
errorMessage = "The date on which the loan is repaid in full cannot be earlier than client's transfer date to this office";
action = "close";
postfix = "cannot.be.undone.before.client.transfer.date";
break;
case LOAN_CHARGE_PAYMENT:
errorMessage = "The date on which a charge payment is made cannot be earlier than client's transfer date to this office";
action = "charge.payment";
postfix = "cannot.be.made.before.client.transfer.date";
break;
case LOAN_REFUND:
errorMessage = "The date on which a refund is made cannot be earlier than client's transfer date to this office";
action = "refund";
postfix = "cannot.be.made.before.client.transfer.date";
break;
case LOAN_DISBURSAL_UNDO_LAST:
errorMessage = "Cannot undo a last disbursal in another branch";
action = "disbursal";
postfix = "cannot.be.undone.before.client.transfer.date";
break;
default:
break;
}
throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, clientOfficeJoiningDate);
}
}
}
private void validateActivityNotBeforeLastTransactionDate(final LoanEvent event, final LocalDate activityDate) {
if (!(this.repaymentScheduleDetail().isInterestRecalculationEnabled() || this.loanProduct().isHoldGuaranteeFundsEnabled())) {
return;
}
LocalDate lastTransactionDate = getLastUserTransactionDate();
if (lastTransactionDate.isAfter(activityDate)) {
String errorMessage = null;
String action = null;
String postfix = null;
switch (event) {
case LOAN_REPAYMENT_OR_WAIVER:
errorMessage = "The date on which a repayment or waiver is made cannot be earlier than last transaction date";
action = "repayment.or.waiver";
postfix = "cannot.be.made.before.last.transaction.date";
break;
case WRITE_OFF_OUTSTANDING:
errorMessage = "The date on which a write off is made cannot be earlier than last transaction date";
action = "writeoff";
postfix = "cannot.be.made.before.last.transaction.date";
break;
case LOAN_CHARGE_PAYMENT:
errorMessage = "The date on which a charge payment is made cannot be earlier than last transaction date";
action = "charge.payment";
postfix = "cannot.be.made.before.last.transaction.date";
break;
default:
break;
}
throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, lastTransactionDate);
}
}
public LocalDate getLastUserTransactionDate() {
LocalDate currentTransactionDate = getDisbursementDate();
for (final LoanTransaction previousTransaction : this.loanTransactions) {
if (!(previousTransaction.isReversed() || previousTransaction.isAccrual() || previousTransaction.isIncomePosting())) {
if (currentTransactionDate.isBefore(previousTransaction.getTransactionDate())) {
currentTransactionDate = previousTransaction.getTransactionDate();
}
}
}
return currentTransactionDate;
}
public LocalDate getLastRepaymentDate() {
LocalDate currentTransactionDate = getDisbursementDate();
for (final LoanTransaction previousTransaction : this.loanTransactions) {
if (previousTransaction.isRepayment()) {
if (currentTransactionDate.isBefore(previousTransaction.getTransactionDate())) {
currentTransactionDate = previousTransaction.getTransactionDate();
}
}
}
return currentTransactionDate;
}
public LocalDate getLastUserTransactionForChargeCalc() {
LocalDate lastTransaction = getDisbursementDate();
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
lastTransaction = getLastUserTransactionDate();
}
return lastTransaction;
}
public Set<LoanCharge> charges() {
Set<LoanCharge> loanCharges = new HashSet<>();
if (this.charges != null) {
for (LoanCharge charge : this.charges) {
if (charge.isActive()) {
loanCharges.add(charge);
}
}
}
return loanCharges;
}
public Set<LoanTrancheCharge> trancheCharges() {
Set<LoanTrancheCharge> loanCharges = new HashSet<>();
if (this.trancheCharges != null) {
for (LoanTrancheCharge charge : this.trancheCharges) {
loanCharges.add(charge);
}
}
return loanCharges;
}
public List<LoanInstallmentCharge> generateInstallmentLoanCharges(final LoanCharge loanCharge) {
final List<LoanInstallmentCharge> loanChargePerInstallments = new ArrayList<>();
if (loanCharge.isInstalmentFee()) {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isRecalculatedInterestComponent()) {
continue;
}
BigDecimal amount = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isFlat()) {
amount = loanCharge.amountOrPercentage();
} else {
amount = calculateInstallmentChargeAmount(loanCharge.getChargeCalculation(), loanCharge.getPercentage(), installment)
.getAmount();
}
final LoanInstallmentCharge loanInstallmentCharge = new LoanInstallmentCharge(amount, loanCharge, installment);
installment.getInstallmentCharges().add(loanInstallmentCharge);
loanChargePerInstallments.add(loanInstallmentCharge);
}
}
return loanChargePerInstallments;
}
public void validateAccountStatus(final LoanEvent event) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
switch (event) {
case LOAN_CREATED:
break;
case LOAN_APPROVED:
if (!isSubmittedAndPendingApproval()) {
final String defaultUserMessage = "Loan Account Approval is not allowed. Loan Account is not in submitted and pending approval state.";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.approve.account.is.not.submitted.and.pending.state", defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_APPROVAL_UNDO:
if (!isApproved()) {
final String defaultUserMessage = "Loan Account Undo Approval is not allowed. Loan Account is not in approved state.";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.undo.approval.account.is.not.approved",
defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_DISBURSED:
if ((!(isApproved() && isNotDisbursed()) && !this.loanProduct.isMultiDisburseLoan())
|| (this.loanProduct.isMultiDisburseLoan() && !isAllTranchesNotDisbursed())) {
final String defaultUserMessage = "Loan Disbursal is not allowed. Loan Account is not in approved and not disbursed state.";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.disbursal.account.is.not.approve.not.disbursed.state", defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_DISBURSAL_UNDO:
if (!isOpen()) {
final String defaultUserMessage = "Loan Undo disbursal is not allowed. Loan Account is not active.";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.undo.disbursal.account.is.not.active",
defaultUserMessage);
dataValidationErrors.add(error);
}
if (isOpen() && this.isTopup()) {
final String defaultUserMessage = "Loan Undo disbursal is not allowed on Topup Loans";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.undo.disbursal.not.allowed.on.topup.loan", defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_REPAYMENT_OR_WAIVER:
if (!isOpen()) {
final String defaultUserMessage = "Loan Repayment or Waiver is not allowed. Loan Account is not active.";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.repayment.or.waiver.account.is.not.active", defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_REJECTED:
if (!isSubmittedAndPendingApproval()) {
final String defaultUserMessage = "Loan application cannot be rejected. Loan Account is not in Submitted and Pending approval state.";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.reject.account.is.not.submitted.pending.approval.state", defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_WITHDRAWN:
if (!isSubmittedAndPendingApproval()) {
final String defaultUserMessage = "Loan application cannot be withdrawn. Loan Account is not in Submitted and Pending approval state.";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.withdrawn.account.is.not.submitted.pending.approval.state", defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case WRITE_OFF_OUTSTANDING:
if (!isOpen()) {
final String defaultUserMessage = "Loan Written off is not allowed. Loan Account is not active.";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.writtenoff.account.is.not.active",
defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case WRITE_OFF_OUTSTANDING_UNDO:
if (!isClosedWrittenOff()) {
final String defaultUserMessage = "Loan Undo Written off is not allowed. Loan Account is not Written off.";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.undo.writtenoff.account.is.not.written.off", defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case REPAID_IN_FULL:
break;
case LOAN_CHARGE_PAYMENT:
if (!isOpen()) {
final String defaultUserMessage = "Charge payment is not allowed. Loan Account is not Active.";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.charge.payment.account.is.not.active",
defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_CLOSED:
if (!isOpen()) {
final String defaultUserMessage = "Closing Loan Account is not allowed. Loan Account is not Active.";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.close.account.is.not.active",
defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_EDIT_MULTI_DISBURSE_DATE:
if (isClosed()) {
final String defaultUserMessage = "Edit disbursement is not allowed. Loan Account is not active.";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.edit.disbursement.account.is.not.active",
defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_RECOVERY_PAYMENT:
if (!isClosedWrittenOff()) {
final String defaultUserMessage = "Recovery repayments may only be made on loans which are written off";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.account.is.not.written.off",
defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_REFUND:
if (!isOpen()) {
final String defaultUserMessage = "Loan Refund is not allowed. Loan Account is not active.";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.refund.account.is.not.active",
defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_DISBURSAL_UNDO_LAST:
if (!isOpen()) {
final String defaultUserMessage = "Loan Undo last disbursal is not allowed. Loan Account is not active.";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.undo.last.disbursal.account.is.not.active", defaultUserMessage);
dataValidationErrors.add(error);
}
break;
case LOAN_FORECLOSURE:
if (!isOpen()) {
final String defaultUserMessage = "Loan foreclosure is not allowed. Loan Account is not active.";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.foreclosure.account.is.not.active",
defaultUserMessage);
dataValidationErrors.add(error);
}
break;
default:
break;
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
public LoanCharge fetchLoanChargesById(Long id) {
LoanCharge charge = null;
for (LoanCharge loanCharge : this.charges) {
if (id.equals(loanCharge.getId())) {
charge = loanCharge;
break;
}
}
return charge;
}
private List<Long> fetchAllLoanChargeIds() {
List<Long> list = new ArrayList<>();
for (LoanCharge loanCharge : this.charges) {
list.add(loanCharge.getId());
}
return list;
}
public List<LoanDisbursementDetails> getDisbursementDetails() {
return this.disbursementDetails;
}
public ChangedTransactionDetail updateDisbursementDateAndAmountForTranche(final LoanDisbursementDetails disbursementDetails,
final JsonCommand command, final Map<String, Object> actualChanges, final ScheduleGeneratorDTO scheduleGeneratorDTO,
final AppUser currentUser) {
final Locale locale = command.extractLocale();
validateAccountStatus(LoanEvent.LOAN_EDIT_MULTI_DISBURSE_DATE);
final BigDecimal principal = command.bigDecimalValueOfParameterNamed(LoanApiConstants.updatedDisbursementPrincipalParameterName,
locale);
final LocalDate expectedDisbursementDate = command
.localDateValueOfParameterNamed(LoanApiConstants.updatedDisbursementDateParameterName);
disbursementDetails.updateExpectedDisbursementDateAndAmount(
Date.from(expectedDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), principal);
actualChanges.put(LoanApiConstants.disbursementDateParameterName,
command.stringValueOfParameterNamed(LoanApiConstants.disbursementDateParameterName));
actualChanges.put(LoanApiConstants.disbursementIdParameterName,
command.stringValueOfParameterNamed(LoanApiConstants.disbursementIdParameterName));
actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName,
command.bigDecimalValueOfParameterNamed(LoanApiConstants.disbursementPrincipalParameterName, locale));
Collection<LoanDisbursementDetails> loanDisburseDetails = this.getDisbursementDetails();
BigDecimal setPrincipalAmount = BigDecimal.ZERO;
for (LoanDisbursementDetails details : loanDisburseDetails) {
if (details.actualDisbursementDate() != null) {
setPrincipalAmount = setPrincipalAmount.add(details.principal());
}
}
this.loanRepaymentScheduleDetail.setPrincipal(setPrincipalAmount);
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO, currentUser);
} else {
regenerateRepaymentSchedule(scheduleGeneratorDTO, currentUser);
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
charges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
addLoanTransaction(mapEntry.getValue());
}
return changedTransactionDetail;
}
public BigDecimal retriveLastEmiAmount() {
BigDecimal emiAmount = this.fixedEmiAmount;
Date startDate = Date.from(this.getDisbursementDate().atStartOfDay(ZoneId.systemDefault()).toInstant());
for (LoanTermVariations loanTermVariations : this.loanTermVariations) {
if (loanTermVariations.getTermType().isEMIAmountVariation() && !startDate.after(loanTermVariations.getTermApplicableFrom())) {
startDate = loanTermVariations.getTermApplicableFrom();
emiAmount = loanTermVariations.getTermValue();
}
}
return emiAmount;
}
public LoanRepaymentScheduleInstallment fetchRepaymentScheduleInstallment(final Integer installmentNumber) {
LoanRepaymentScheduleInstallment installment = null;
if (installmentNumber == null) {
return installment;
}
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment scheduleInstallment : installments) {
if (scheduleInstallment.getInstallmentNumber().equals(installmentNumber)) {
installment = scheduleInstallment;
break;
}
}
return installment;
}
public BigDecimal getApprovedPrincipal() {
return this.approvedPrincipal;
}
public BigDecimal getNetDisbursalAmount() {
return netDisbursalAmount;
}
public BigDecimal deductFromNetDisbursalAmount(final BigDecimal subtrahend) {
this.netDisbursalAmount = this.netDisbursalAmount.subtract(subtrahend);
return netDisbursalAmount;
}
public void setNetDisbursalAmount(BigDecimal netDisbursalAmount) {
this.netDisbursalAmount = netDisbursalAmount;
}
public BigDecimal getTotalOverpaid() {
return this.totalOverpaid;
}
public void updateIsInterestRecalculationEnabled() {
this.loanRepaymentScheduleDetail.updateIsInterestRecalculationEnabled(isInterestRecalculationEnabledForProduct());
}
public LoanInterestRecalculationDetails loanInterestRecalculationDetails() {
return this.loanInterestRecalculationDetails;
}
public Long loanInterestRecalculationDetailId() {
if (loanInterestRecalculationDetails() != null) {
return this.loanInterestRecalculationDetails.getId();
}
return null;
}
public LocalDate getExpectedMaturityDate() {
LocalDate expectedMaturityDate = null;
if (this.expectedMaturityDate != null) {
expectedMaturityDate = LocalDate.ofInstant(this.expectedMaturityDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
return expectedMaturityDate;
}
public LocalDate getMaturityDate() {
LocalDate maturityDate = getExpectedMaturityDate();
if (this.actualMaturityDate != null) {
maturityDate = LocalDate.ofInstant(this.actualMaturityDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
return maturityDate;
}
public ChangedTransactionDetail recalculateScheduleFromLastTransaction(final ScheduleGeneratorDTO generatorDTO,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final AppUser currentUser) {
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
/*
* LocalDate recalculateFrom = null; List<LoanTransaction> loanTransactions =
* this.retreiveListOfTransactionsPostDisbursementExcludeAccruals(); for (LoanTransaction loanTransaction :
* loanTransactions) { if (recalculateFrom == null ||
* loanTransaction.getTransactionDate().isAfter(recalculateFrom)) { recalculateFrom =
* loanTransaction.getTransactionDate(); } } generatorDTO.setRecalculateFrom(recalculateFrom);
*/
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO, currentUser);
} else {
regenerateRepaymentSchedule(generatorDTO, currentUser);
}
return processTransactions();
}
public ChangedTransactionDetail handleRegenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO,
final AppUser currentUser) {
regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO, currentUser);
return processTransactions();
}
public ChangedTransactionDetail processTransactions() {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
charges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
/***
* Commented since throwing exception if external id present for one of the transactions. for this need to save
* the reversed transactions first and then new transactions.
*/
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
updateLoanSummaryDerivedFields();
this.loanTransactions.removeAll(changedTransactionDetail.getNewTransactionMappings().values());
return changedTransactionDetail;
}
public void regenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO, final AppUser currentUser) {
LocalDate lastTransactionDate = getLastUserTransactionDate();
final LoanScheduleDTO loanSchedule = getRecalculatedSchedule(generatorDTO);
if (loanSchedule == null) {
return;
}
updateLoanSchedule(loanSchedule.getInstallments(), currentUser);
this.interestRecalculatedOn = DateUtils.getDateOfTenant();
LocalDate lastRepaymentDate = this.getLastRepaymentPeriodDueDate(true);
Set<LoanCharge> charges = this.charges();
for (final LoanCharge loanCharge : charges) {
if (!loanCharge.isDueAtDisbursement()) {
updateOverdueScheduleInstallment(loanCharge);
if (loanCharge.getDueLocalDate() == null || !lastRepaymentDate.isBefore(loanCharge.getDueLocalDate())) {
if ((loanCharge.isInstalmentFee() || !loanCharge.isWaived())
&& (loanCharge.getDueLocalDate() == null || !lastTransactionDate.isAfter(loanCharge.getDueLocalDate()))) {
recalculateLoanCharge(loanCharge, generatorDTO.getPenaltyWaitPeriod());
loanCharge.updateWaivedAmount(getCurrency());
}
} else {
loanCharge.setActive(false);
}
}
}
processPostDisbursementTransactions();
processIncomeTransactions(currentUser);
}
private void updateLoanChargesPaidBy(LoanTransaction accrual, HashMap<String, Object> feeDetails,
LoanRepaymentScheduleInstallment installment) {
@SuppressWarnings("unchecked")
List<LoanCharge> loanCharges = (List<LoanCharge>) feeDetails.get("loanCharges");
@SuppressWarnings("unchecked")
List<LoanInstallmentCharge> loanInstallmentCharges = (List<LoanInstallmentCharge>) feeDetails.get("loanInstallmentCharges");
if (loanCharges != null) {
for (LoanCharge loanCharge : loanCharges) {
Integer installmentNumber = null == installment ? null : installment.getInstallmentNumber();
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrual, loanCharge,
loanCharge.getAmount(getCurrency()).getAmount(), installmentNumber);
accrual.getLoanChargesPaid().add(loanChargePaidBy);
}
}
if (loanInstallmentCharges != null) {
for (LoanInstallmentCharge loanInstallmentCharge : loanInstallmentCharges) {
Integer installmentNumber = null == loanInstallmentCharge.getInstallment() ? null
: loanInstallmentCharge.getInstallment().getInstallmentNumber();
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrual, loanInstallmentCharge.getLoancharge(),
loanInstallmentCharge.getAmount(getCurrency()).getAmount(), installmentNumber);
accrual.getLoanChargesPaid().add(loanChargePaidBy);
}
}
}
public void processIncomeTransactions(AppUser currentUser) {
if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()) {
LocalDate lastCompoundingDate = this.getDisbursementDate();
List<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = extractInterestRecalculationAdditionalDetails();
List<LoanTransaction> incomeTransactions = retreiveListOfIncomePostingTransactions();
List<LoanTransaction> accrualTransactions = retreiveListOfAccrualTransactions();
for (LoanInterestRecalcualtionAdditionalDetails compoundingDetail : compoundingDetails) {
if (!compoundingDetail.getEffectiveDate().isBefore(DateUtils.getLocalDateOfTenant())) {
break;
}
LoanTransaction incomeTransaction = getTransactionForDate(incomeTransactions, compoundingDetail.getEffectiveDate());
LoanTransaction accrualTransaction = getTransactionForDate(accrualTransactions, compoundingDetail.getEffectiveDate());
addUpdateIncomeAndAccrualTransaction(compoundingDetail, lastCompoundingDate, currentUser, incomeTransaction,
accrualTransaction);
lastCompoundingDate = compoundingDetail.getEffectiveDate();
}
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1);
reverseTransactionsPostEffectiveDate(incomeTransactions, lastInstallment.getDueDate());
reverseTransactionsPostEffectiveDate(accrualTransactions, lastInstallment.getDueDate());
}
}
private void reverseTransactionsOnOrAfter(List<LoanTransaction> transactions, Date date) {
LocalDate refDate = LocalDate.ofInstant(date.toInstant(), DateUtils.getDateTimeZoneOfTenant());
for (LoanTransaction loanTransaction : transactions) {
if (!loanTransaction.getTransactionDate().isBefore(refDate)) {
loanTransaction.reverse();
}
}
}
private void addUpdateIncomeAndAccrualTransaction(LoanInterestRecalcualtionAdditionalDetails compoundingDetail,
LocalDate lastCompoundingDate, AppUser currentUser, LoanTransaction existingIncomeTransaction,
LoanTransaction existingAccrualTransaction) {
BigDecimal interest = BigDecimal.ZERO;
BigDecimal fee = BigDecimal.ZERO;
BigDecimal penalties = BigDecimal.ZERO;
HashMap<String, Object> feeDetails = new HashMap<>();
if (this.loanInterestRecalculationDetails.getInterestRecalculationCompoundingMethod()
.equals(InterestRecalculationCompoundingMethod.INTEREST)) {
interest = compoundingDetail.getAmount();
} else if (this.loanInterestRecalculationDetails.getInterestRecalculationCompoundingMethod()
.equals(InterestRecalculationCompoundingMethod.FEE)) {
determineFeeDetails(lastCompoundingDate, compoundingDetail.getEffectiveDate(), feeDetails);
fee = (BigDecimal) feeDetails.get("fee");
penalties = (BigDecimal) feeDetails.get("penalties");
} else if (this.loanInterestRecalculationDetails.getInterestRecalculationCompoundingMethod()
.equals(InterestRecalculationCompoundingMethod.INTEREST_AND_FEE)) {
determineFeeDetails(lastCompoundingDate, compoundingDetail.getEffectiveDate(), feeDetails);
fee = (BigDecimal) feeDetails.get("fee");
penalties = (BigDecimal) feeDetails.get("penalties");
interest = compoundingDetail.getAmount().subtract(fee).subtract(penalties);
}
if (existingIncomeTransaction == null) {
LoanTransaction transaction = LoanTransaction.incomePosting(this, this.getOffice(),
Date.from(compoundingDetail.getEffectiveDate().atStartOfDay(ZoneId.systemDefault()).toInstant()),
compoundingDetail.getAmount(), interest, fee, penalties, currentUser);
addLoanTransaction(transaction);
} else if (existingIncomeTransaction.getAmount(getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) {
existingIncomeTransaction.reverse();
LoanTransaction transaction = LoanTransaction.incomePosting(this, this.getOffice(),
Date.from(compoundingDetail.getEffectiveDate().atStartOfDay(ZoneId.systemDefault()).toInstant()),
compoundingDetail.getAmount(), interest, fee, penalties, currentUser);
addLoanTransaction(transaction);
}
if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
if (existingAccrualTransaction == null) {
LoanTransaction accrual = LoanTransaction.accrueTransaction(this, this.getOffice(), compoundingDetail.getEffectiveDate(),
compoundingDetail.getAmount(), interest, fee, penalties, currentUser);
updateLoanChargesPaidBy(accrual, feeDetails, null);
addLoanTransaction(accrual);
} else if (existingAccrualTransaction.getAmount(getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) {
existingAccrualTransaction.reverse();
LoanTransaction accrual = LoanTransaction.accrueTransaction(this, this.getOffice(), compoundingDetail.getEffectiveDate(),
compoundingDetail.getAmount(), interest, fee, penalties, currentUser);
updateLoanChargesPaidBy(accrual, feeDetails, null);
addLoanTransaction(accrual);
}
}
updateLoanOutstandingBalaces();
}
private void determineFeeDetails(LocalDate fromDate, LocalDate toDate, HashMap<String, Object> feeDetails) {
BigDecimal fee = BigDecimal.ZERO;
BigDecimal penalties = BigDecimal.ZERO;
List<Integer> installments = new ArrayList<>();
List<LoanRepaymentScheduleInstallment> repaymentSchedule = getRepaymentScheduleInstallments();
for (LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment : repaymentSchedule) {
if (loanRepaymentScheduleInstallment.getDueDate().isAfter(fromDate)
&& !loanRepaymentScheduleInstallment.getDueDate().isAfter(toDate)) {
installments.add(loanRepaymentScheduleInstallment.getInstallmentNumber());
}
}
List<LoanCharge> loanCharges = new ArrayList<>();
List<LoanInstallmentCharge> loanInstallmentCharges = new ArrayList<>();
for (LoanCharge loanCharge : this.charges()) {
if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(fromDate, toDate)) {
if (loanCharge.isPenaltyCharge() && !loanCharge.isInstalmentFee()) {
penalties = penalties.add(loanCharge.amount());
loanCharges.add(loanCharge);
} else if (!loanCharge.isInstalmentFee()) {
fee = fee.add(loanCharge.amount());
loanCharges.add(loanCharge);
}
} else if (loanCharge.isInstalmentFee()) {
for (LoanInstallmentCharge installmentCharge : loanCharge.installmentCharges()) {
if (installments.contains(installmentCharge.getRepaymentInstallment().getInstallmentNumber())) {
fee = fee.add(installmentCharge.getAmount());
loanInstallmentCharges.add(installmentCharge);
}
}
}
}
feeDetails.put("fee", fee);
feeDetails.put("penalties", penalties);
feeDetails.put("loanCharges", loanCharges);
feeDetails.put("loanInstallmentCharges", loanInstallmentCharges);
}
private LoanTransaction getTransactionForDate(List<LoanTransaction> transactions, LocalDate effectiveDate) {
for (LoanTransaction loanTransaction : transactions) {
if (loanTransaction.getTransactionDate().isEqual(effectiveDate)) {
return loanTransaction;
}
}
return null;
}
private void reverseTransactionsPostEffectiveDate(List<LoanTransaction> transactions, LocalDate effectiveDate) {
for (LoanTransaction loanTransaction : transactions) {
if (loanTransaction.getTransactionDate().isAfter(effectiveDate)) {
loanTransaction.reverse();
}
}
}
private List<LoanInterestRecalcualtionAdditionalDetails> extractInterestRecalculationAdditionalDetails() {
List<LoanInterestRecalcualtionAdditionalDetails> retDetails = new ArrayList<>();
List<LoanRepaymentScheduleInstallment> repaymentSchedule = getRepaymentScheduleInstallments();
if (null != this.repaymentScheduleInstallments && this.repaymentScheduleInstallments.size() > 0) {
Iterator<LoanRepaymentScheduleInstallment> installmentsItr = repaymentSchedule.iterator();
while (installmentsItr.hasNext()) {
LoanRepaymentScheduleInstallment installment = installmentsItr.next();
if (null != installment.getLoanCompoundingDetails()) {
retDetails.addAll(installment.getLoanCompoundingDetails());
}
}
}
Collections.sort(retDetails, new Comparator<LoanInterestRecalcualtionAdditionalDetails>() {
@Override
public int compare(LoanInterestRecalcualtionAdditionalDetails first, LoanInterestRecalcualtionAdditionalDetails second) {
return first.getEffectiveDate().compareTo(second.getEffectiveDate());
}
});
return retDetails;
}
public void processPostDisbursementTransactions() {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> copyTransactions = new ArrayList<>();
if (allNonContraTransactionsPostDisbursement.size() > 0) {
for (LoanTransaction loanTransaction : allNonContraTransactionsPostDisbursement) {
copyTransactions.add(LoanTransaction.copyTransactionProperties(loanTransaction));
}
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), copyTransactions, getCurrency(),
getRepaymentScheduleInstallments(), charges());
updateLoanSummaryDerivedFields();
}
}
private LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO generatorDTO) {
if (!this.repaymentScheduleDetail().isInterestRecalculationEnabled() || isNpa) {
return null;
}
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanScheduleGenerator loanScheduleGenerator = generatorDTO.getLoanScheduleFactory().create(interestMethod);
final RoundingMode roundingMode = MoneyHelper.getRoundingMode();
final MathContext mc = new MathContext(8, roundingMode);
final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(generatorDTO);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
return loanScheduleGenerator.rescheduleNextInstallments(mc, loanApplicationTerms, this, generatorDTO.getHolidayDetailDTO(),
loanRepaymentScheduleTransactionProcessor, generatorDTO.getRecalculateFrom());
}
public LoanRepaymentScheduleInstallment fetchPrepaymentDetail(final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate onDate) {
LoanRepaymentScheduleInstallment installment = null;
if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) {
final RoundingMode roundingMode = MoneyHelper.getRoundingMode();
final MathContext mc = new MathContext(8, roundingMode);
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(scheduleGeneratorDTO);
final LoanScheduleGenerator loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(interestMethod);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
installment = loanScheduleGenerator.calculatePrepaymentAmount(getCurrency(), onDate, loanApplicationTerms, mc, this,
scheduleGeneratorDTO.getHolidayDetailDTO(), loanRepaymentScheduleTransactionProcessor);
} else {
installment = this.getTotalOutstandingOnLoan();
}
return installment;
}
public LoanApplicationTerms constructLoanApplicationTerms(final ScheduleGeneratorDTO scheduleGeneratorDTO) {
final Integer loanTermFrequency = this.termFrequency;
final PeriodFrequencyType loanTermPeriodFrequencyType = PeriodFrequencyType.fromInt(this.termPeriodFrequencyType);
NthDayType nthDayType = null;
DayOfWeekType dayOfWeekType = null;
final List<DisbursementData> disbursementData = new ArrayList<>();
for (LoanDisbursementDetails disbursementDetails : this.disbursementDetails) {
disbursementData.add(disbursementDetails.toData());
}
Calendar calendar = scheduleGeneratorDTO.getCalendar();
if (calendar != null) {
nthDayType = CalendarUtils.getRepeatsOnNthDayOfMonth(calendar.getRecurrence());
dayOfWeekType = DayOfWeekType.fromInt(CalendarUtils.getRepeatsOnDay(calendar.getRecurrence()).getValue());
}
HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO();
CalendarInstance restCalendarInstance = null;
CalendarInstance compoundingCalendarInstance = null;
RecalculationFrequencyType recalculationFrequencyType = null;
InterestRecalculationCompoundingMethod compoundingMethod = null;
RecalculationFrequencyType compoundingFrequencyType = null;
LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
boolean allowCompoundingOnEod = false;
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = scheduleGeneratorDTO.getCalendarInstanceForInterestRecalculation();
compoundingCalendarInstance = scheduleGeneratorDTO.getCompoundingCalendarInstance();
recalculationFrequencyType = this.loanInterestRecalculationDetails.getRestFrequencyType();
compoundingMethod = this.loanInterestRecalculationDetails.getInterestRecalculationCompoundingMethod();
compoundingFrequencyType = this.loanInterestRecalculationDetails.getCompoundingFrequencyType();
rescheduleStrategyMethod = this.loanInterestRecalculationDetails.getRescheduleStrategyMethod();
allowCompoundingOnEod = this.loanInterestRecalculationDetails.allowCompoundingOnEod();
calendarHistoryDataWrapper = scheduleGeneratorDTO.getCalendarHistoryDataWrapper();
}
calendar = scheduleGeneratorDTO.getCalendar();
calendarHistoryDataWrapper = scheduleGeneratorDTO.getCalendarHistoryDataWrapper();
BigDecimal annualNominalInterestRate = this.loanRepaymentScheduleDetail.getAnnualNominalInterestRate();
FloatingRateDTO floatingRateDTO = scheduleGeneratorDTO.getFloatingRateDTO();
List<LoanTermVariationsData> loanTermVariations = new ArrayList<>();
annualNominalInterestRate = constructLoanTermVariations(floatingRateDTO, annualNominalInterestRate, loanTermVariations);
LocalDate interestChargedFromDate = getInterestChargedFromDate();
if (interestChargedFromDate == null && scheduleGeneratorDTO.isInterestChargedFromDateAsDisbursementDateEnabled()) {
interestChargedFromDate = getDisbursementDate();
}
final LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(scheduleGeneratorDTO.getApplicationCurrency(),
loanTermFrequency, loanTermPeriodFrequencyType, nthDayType, dayOfWeekType, getDisbursementDate(),
getExpectedFirstRepaymentOnDate(), scheduleGeneratorDTO.getCalculatedRepaymentsStartingFromDate(), getInArrearsTolerance(),
this.loanRepaymentScheduleDetail, this.loanProduct.isMultiDisburseLoan(), this.fixedEmiAmount, disbursementData,
this.maxOutstandingLoanBalance, interestChargedFromDate, this.loanProduct.getPrincipalThresholdForLastInstallment(),
this.loanProduct.getInstallmentAmountInMultiplesOf(), recalculationFrequencyType, restCalendarInstance, compoundingMethod,
compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(),
rescheduleStrategyMethod, calendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations,
calendarHistoryDataWrapper, scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(),
holidayDetailDTO, allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday(),
scheduleGeneratorDTO.isInterestToBeRecoveredFirstWhenGreaterThanEMI(), this.fixedPrincipalPercentagePerInstallment,
scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans());
return loanApplicationTerms;
}
public BigDecimal constructLoanTermVariations(FloatingRateDTO floatingRateDTO, BigDecimal annualNominalInterestRate,
List<LoanTermVariationsData> loanTermVariations) {
for (LoanTermVariations variationTerms : this.loanTermVariations) {
if (variationTerms.isActive()) {
loanTermVariations.add(variationTerms.toData());
}
}
annualNominalInterestRate = constructFloatingInterestRates(annualNominalInterestRate, floatingRateDTO, loanTermVariations);
return annualNominalInterestRate;
}
private LoanRepaymentScheduleInstallment getTotalOutstandingOnLoan() {
Money feeCharges = Money.zero(loanCurrency());
Money penaltyCharges = Money.zero(loanCurrency());
Money totalPrincipal = Money.zero(loanCurrency());
Money totalInterest = Money.zero(loanCurrency());
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null;
List<LoanRepaymentScheduleInstallment> repaymentSchedule = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment scheduledRepayment : repaymentSchedule) {
totalPrincipal = totalPrincipal.plus(scheduledRepayment.getPrincipalOutstanding(loanCurrency()));
totalInterest = totalInterest.plus(scheduledRepayment.getInterestOutstanding(loanCurrency()));
feeCharges = feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(loanCurrency()));
penaltyCharges = penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(loanCurrency()));
}
return new LoanRepaymentScheduleInstallment(null, 0, LocalDate.now(DateUtils.getDateTimeZoneOfTenant()),
LocalDate.now(DateUtils.getDateTimeZoneOfTenant()), totalPrincipal.getAmount(), totalInterest.getAmount(),
feeCharges.getAmount(), penaltyCharges.getAmount(), false, compoundingDetails);
}
public LocalDate getAccruedTill() {
LocalDate accruedTill = null;
if (this.accruedTill != null) {
accruedTill = LocalDate.ofInstant(this.accruedTill.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
return accruedTill;
}
public LocalDate fetchInterestRecalculateFromDate() {
LocalDate interestRecalculatedOn = null;
if (this.interestRecalculatedOn == null) {
interestRecalculatedOn = getDisbursementDate();
} else {
interestRecalculatedOn = LocalDate.ofInstant(this.interestRecalculatedOn.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
return interestRecalculatedOn;
}
private void updateLoanOutstandingBalaces() {
Money outstanding = Money.zero(getCurrency());
List<LoanTransaction> loanTransactions = retreiveListOfTransactionsExcludeAccruals();
for (LoanTransaction loanTransaction : loanTransactions) {
if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting()) {
outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency()));
loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount());
} else {
if (this.loanInterestRecalculationDetails != null
&& this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()
&& !loanTransaction.isRepaymentAtDisbursement()) {
outstanding = outstanding.minus(loanTransaction.getAmount(getCurrency()));
} else {
outstanding = outstanding.minus(loanTransaction.getPrincipalPortion(getCurrency()));
}
loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount());
}
}
}
public LoanTransactionProcessingStrategy transactionProcessingStrategy() {
return this.transactionProcessingStrategy;
}
public boolean isNpa() {
return this.isNpa;
}
/**
* @return List of loan repayments schedule objects
**/
public List<LoanRepaymentScheduleInstallment> getRepaymentScheduleInstallments() {
return this.repaymentScheduleInstallments;
}
public Integer getLoanRepaymentScheduleInstallmentsSize() {
return this.repaymentScheduleInstallments.size();
}
public void addLoanRepaymentScheduleInstallment(final LoanRepaymentScheduleInstallment installment) {
installment.updateLoan(this);
this.repaymentScheduleInstallments.add(installment);
}
/**
* @return Loan product minimum repayments schedule related detail
**/
public LoanProductRelatedDetail getLoanRepaymentScheduleDetail() {
return this.loanRepaymentScheduleDetail;
}
/**
* @return Loan Fixed Emi amount
**/
public BigDecimal getFixedEmiAmount() {
return this.fixedEmiAmount;
}
/**
* @return maximum outstanding loan balance
**/
public BigDecimal getMaxOutstandingLoanBalance() {
return this.maxOutstandingLoanBalance;
}
/**
* @param dueDate
* the due date of the installment
* @return a schedule installment with similar due date to the one provided
**/
public LoanRepaymentScheduleInstallment getRepaymentScheduleInstallment(LocalDate dueDate) {
LoanRepaymentScheduleInstallment installment = null;
if (dueDate != null) {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (LoanRepaymentScheduleInstallment repaymentScheduleInstallment : installments) {
if (repaymentScheduleInstallment.getDueDate().isEqual(dueDate)) {
installment = repaymentScheduleInstallment;
break;
}
}
}
return installment;
}
/**
* @return loan disbursement data
**/
public List<DisbursementData> getDisbursmentData() {
Iterator<LoanDisbursementDetails> iterator = this.getDisbursementDetails().iterator();
List<DisbursementData> disbursementData = new ArrayList<>();
while (iterator.hasNext()) {
LoanDisbursementDetails loanDisbursementDetails = iterator.next();
LocalDate expectedDisbursementDate = null;
LocalDate actualDisbursementDate = null;
if (loanDisbursementDetails.expectedDisbursementDate() != null) {
expectedDisbursementDate = LocalDate.ofInstant(loanDisbursementDetails.expectedDisbursementDate().toInstant(),
DateUtils.getDateTimeZoneOfTenant());
}
if (loanDisbursementDetails.actualDisbursementDate() != null) {
actualDisbursementDate = LocalDate.ofInstant(loanDisbursementDetails.actualDisbursementDate().toInstant(),
DateUtils.getDateTimeZoneOfTenant());
}
BigDecimal waivedChargeAmount = null;
disbursementData.add(new DisbursementData(loanDisbursementDetails.getId(), expectedDisbursementDate, actualDisbursementDate,
loanDisbursementDetails.principal(), this.netDisbursalAmount, null, null, waivedChargeAmount));
}
return disbursementData;
}
/**
* @param applicationCurrency
* @param restCalendarInstance
* TODO
* @param compoundingCalendarInstance
* TODO
* @param loanCalendar
* @param floatingRateDTO
* TODO
* @param isSkipRepaymentonmonthFirst
* @param numberofdays
* @param holidayDetailDTO
* Used for accessing the loan's calendar object
* @return application terms of the Loan object
**/
@SuppressWarnings({ "unused" })
public LoanApplicationTerms getLoanApplicationTerms(final ApplicationCurrency applicationCurrency,
final CalendarInstance restCalendarInstance, CalendarInstance compoundingCalendarInstance, final Calendar loanCalendar,
final FloatingRateDTO floatingRateDTO, final boolean isSkipRepaymentonmonthFirst, final Integer numberofdays,
final HolidayDetailDTO holidayDetailDTO) {
LoanProduct loanProduct = loanProduct();
// LoanProductRelatedDetail loanProductRelatedDetail =
// getLoanRepaymentScheduleDetail();
final MonetaryCurrency currency = this.loanRepaymentScheduleDetail.getCurrency();
final Integer loanTermFrequency = getTermFrequency();
final PeriodFrequencyType loanTermPeriodFrequencyType = this.loanRepaymentScheduleDetail.getInterestPeriodFrequencyType();
NthDayType nthDayType = null;
DayOfWeekType dayOfWeekType = null;
if (loanCalendar != null) {
nthDayType = CalendarUtils.getRepeatsOnNthDayOfMonth(loanCalendar.getRecurrence());
CalendarWeekDaysType getRepeatsOnDay = CalendarUtils.getRepeatsOnDay(loanCalendar.getRecurrence());
Integer getRepeatsOnDayValue = null;
if (getRepeatsOnDay != null) {
getRepeatsOnDayValue = getRepeatsOnDay.getValue();
}
if (getRepeatsOnDayValue != null) {
dayOfWeekType = DayOfWeekType.fromInt(getRepeatsOnDayValue);
}
}
final Integer numberOfRepayments = this.loanRepaymentScheduleDetail.getNumberOfRepayments();
final Integer repaymentEvery = this.loanRepaymentScheduleDetail.getRepayEvery();
final PeriodFrequencyType repaymentPeriodFrequencyType = this.loanRepaymentScheduleDetail.getRepaymentPeriodFrequencyType();
final AmortizationMethod amortizationMethod = this.loanRepaymentScheduleDetail.getAmortizationMethod();
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = this.loanRepaymentScheduleDetail
.getInterestCalculationPeriodMethod();
final BigDecimal interestRatePerPeriod = this.loanRepaymentScheduleDetail.getNominalInterestRatePerPeriod();
final PeriodFrequencyType interestRatePeriodFrequencyType = this.loanRepaymentScheduleDetail.getInterestPeriodFrequencyType();
BigDecimal annualNominalInterestRate = this.loanRepaymentScheduleDetail.getAnnualNominalInterestRate();
final Money principalMoney = this.loanRepaymentScheduleDetail.getPrincipal();
final LocalDate expectedDisbursementDate = getExpectedDisbursedOnLocalDate();
final LocalDate repaymentsStartingFromDate = getExpectedFirstRepaymentOnDate();
LocalDate calculatedRepaymentsStartingFromDate = repaymentsStartingFromDate;
// TODO get calender linked to loan if any exist. As of 17-07-2014,
// could not find a link in the database.
// skip for now and set the Calender object to null
// Calendar loanCalendar = null;
// The calendar instance might be null if the loan is not connected
// To a calendar object
// if (loanCalendarInstance != null) {
// loanCalendar = loanCalendarInstance.getCalendar();
// }
final Integer graceOnPrincipalPayment = this.loanRepaymentScheduleDetail.graceOnPrincipalPayment();
final Integer graceOnInterestPayment = this.loanRepaymentScheduleDetail.graceOnInterestPayment();
final Integer graceOnInterestCharged = this.loanRepaymentScheduleDetail.graceOnInterestCharged();
final LocalDate interestChargedFromDate = getInterestChargedFromDate();
final Integer graceOnArrearsAgeing = this.loanRepaymentScheduleDetail.getGraceOnDueDate();
final Money inArrearsToleranceMoney = this.loanRepaymentScheduleDetail.getInArrearsTolerance();
final BigDecimal emiAmount = getFixedEmiAmount();
final BigDecimal maxOutstandingBalance = getMaxOutstandingLoanBalance();
final List<DisbursementData> disbursementData = getDisbursmentData();
CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
if (loanCalendar != null) {
Set<CalendarHistory> calendarHistory = loanCalendar.getCalendarHistory();
calendarHistoryDataWrapper = new CalendarHistoryDataWrapper(calendarHistory);
}
RecalculationFrequencyType recalculationFrequencyType = null;
InterestRecalculationCompoundingMethod compoundingMethod = null;
RecalculationFrequencyType compoundingFrequencyType = null;
LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
boolean allowCompoundingOnEod = false;
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
recalculationFrequencyType = this.loanInterestRecalculationDetails.getRestFrequencyType();
compoundingMethod = this.loanInterestRecalculationDetails.getInterestRecalculationCompoundingMethod();
compoundingFrequencyType = this.loanInterestRecalculationDetails.getCompoundingFrequencyType();
rescheduleStrategyMethod = this.loanInterestRecalculationDetails.getRescheduleStrategyMethod();
allowCompoundingOnEod = this.loanInterestRecalculationDetails.allowCompoundingOnEod();
}
List<LoanTermVariationsData> loanTermVariations = new ArrayList<>();
annualNominalInterestRate = constructFloatingInterestRates(annualNominalInterestRate, floatingRateDTO, loanTermVariations);
return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, nthDayType,
dayOfWeekType, expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate,
inArrearsToleranceMoney, this.loanRepaymentScheduleDetail, loanProduct.isMultiDisburseLoan(), emiAmount, disbursementData,
maxOutstandingBalance, interestChargedFromDate, this.loanProduct.getPrincipalThresholdForLastInstallment(),
this.loanProduct.getInstallmentAmountInMultiplesOf(), recalculationFrequencyType, restCalendarInstance, compoundingMethod,
compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(),
rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations,
calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod, false,
false, this.fixedPrincipalPercentagePerInstallment, false);
}
/**
* @return Loan summary embedded object
**/
public LoanSummary getLoanSummary() {
return this.summary;
}
public void updateRescheduledByUser(AppUser rescheduledByUser) {
this.rescheduledByUser = rescheduledByUser;
}
public LoanProductRelatedDetail getLoanProductRelatedDetail() {
return this.loanRepaymentScheduleDetail;
}
public void updateNumberOfRepayments(Integer numberOfRepayments) {
this.loanRepaymentScheduleDetail.updateNumberOfRepayments(numberOfRepayments);
}
public void updateRescheduledOnDate(LocalDate rescheduledOnDate) {
if (rescheduledOnDate != null) {
this.rescheduledOnDate = Date.from(rescheduledOnDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
}
}
public void updateTermFrequency(Integer termFrequency) {
if (termFrequency != null) {
this.termFrequency = termFrequency;
}
}
public boolean isFeeCompoundingEnabledForInterestRecalculation() {
boolean isEnabled = false;
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
isEnabled = this.loanInterestRecalculationDetails.getInterestRecalculationCompoundingMethod().isFeeCompoundingEnabled();
}
return isEnabled;
}
public String getAccountNumber() {
return this.accountNumber;
}
public Client getClient() {
return this.client;
}
public Boolean shouldCreateStandingInstructionAtDisbursement() {
return this.createStandingInstructionAtDisbursement != null && this.createStandingInstructionAtDisbursement;
}
public Collection<LoanCharge> getLoanCharges(LocalDate dueDate) {
Collection<LoanCharge> loanCharges = new ArrayList<>();
for (LoanCharge loanCharge : charges) {
if (loanCharge.getDueLocalDate() != null && loanCharge.getDueLocalDate().equals(dueDate)) {
loanCharges.add(loanCharge);
}
}
return loanCharges;
}
public void setGuaranteeAmount(BigDecimal guaranteeAmountDerived) {
this.guaranteeAmountDerived = guaranteeAmountDerived;
}
public void updateGuaranteeAmount(BigDecimal guaranteeAmount) {
this.guaranteeAmountDerived = getGuaranteeAmount().add(guaranteeAmount);
}
public BigDecimal getGuaranteeAmount() {
return this.guaranteeAmountDerived == null ? BigDecimal.ZERO : this.guaranteeAmountDerived;
}
public ChangedTransactionDetail makeRefundForActiveLoan(final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List<Holiday> holidays,
final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) {
validateAccountStatus(LoanEvent.LOAN_REFUND);
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REFUND, loanTransaction.getTransactionDate());
validateRefundDateIsAfterLastRepayment(loanTransaction.getTransactionDate());
validateRepaymentDateIsOnHoliday(loanTransaction.getTransactionDate(), allowTransactionsOnHoliday, holidays);
validateRepaymentDateIsOnNonWorkingDay(loanTransaction.getTransactionDate(), workingDays, allowTransactionsOnNonWorkingDay);
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final ChangedTransactionDetail changedTransactionDetail = handleRefundTransaction(loanTransaction, loanLifecycleStateMachine, null);
return changedTransactionDetail;
}
private void validateRefundDateIsAfterLastRepayment(final LocalDate refundTransactionDate) {
final LocalDate possibleNextRefundDate = possibleNextRefundDate();
if (possibleNextRefundDate == null || refundTransactionDate.isBefore(possibleNextRefundDate)) {
throw new InvalidRefundDateException(refundTransactionDate.toString());
}
}
private ChangedTransactionDetail handleRefundTransaction(final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction) {
ChangedTransactionDetail changedTransactionDetail = null;
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REFUND, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
loanTransaction.updateLoan(this);
// final boolean isTransactionChronologicallyLatest =
// isChronologicallyLatestRefund(loanTransaction,
// this.loanTransactions);
if (status().isOverpaid() || status().isClosed()) {
final String errorMessage = "This refund option is only for active loans ";
throw new InvalidLoanStateTransitionException("transaction", "is.exceeding.overpaid.amount", errorMessage, this.totalOverpaid,
loanTransaction.getAmount(getCurrency()).getAmount());
} else if (this.getTotalPaidInRepayments().isZero()) {
final String errorMessage = "Cannot refund when no payment has been made";
throw new InvalidLoanStateTransitionException("transaction", "no.payment.yet.made.for.loan", errorMessage);
}
if (loanTransaction.isNotZero(loanCurrency())) {
addLoanTransaction(loanTransaction);
}
if (loanTransaction.isNotRefundForActiveLoan()) {
final String errorMessage = "A transaction of type refund was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.refund.transaction", errorMessage);
}
final LocalDate loanTransactionDate = loanTransaction.getTransactionDate();
if (loanTransactionDate.isBefore(getDisbursementDate())) {
final String errorMessage = "The transaction date cannot be before the loan disbursement date: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage,
loanTransactionDate, getDisbursementDate());
}
if (loanTransactionDate.isAfter(DateUtils.getLocalDateOfTenant())) {
final String errorMessage = "The transaction date cannot be in the future.";
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, loanTransactionDate);
}
if (this.loanProduct.isMultiDisburseLoan() && adjustedTransaction == null) {
BigDecimal totalDisbursed = getDisbursedAmount();
if (totalDisbursed.compareTo(this.summary.getTotalPrincipalRepaid()) < 0) {
final String errorMessage = "The transaction cannot be done before the loan disbursement: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.done.before.disbursement", errorMessage);
}
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
// If is a refund
if (adjustedTransaction == null) {
loanRepaymentScheduleTransactionProcessor.handleRefund(loanTransaction, getCurrency(), getRepaymentScheduleInstallments(),
charges());
} else {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), charges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
}
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
return changedTransactionDetail;
}
public LocalDate possibleNextRefundDate() {
final LocalDate now = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
LocalDate lastTransactionDate = null;
for (final LoanTransaction transaction : this.loanTransactions) {
if ((transaction.isRepayment() || transaction.isRefundForActiveLoan()) && transaction.isNonZero()) {
lastTransactionDate = transaction.getTransactionDate();
}
}
return lastTransactionDate == null ? now : lastTransactionDate;
}
private Date getActualDisbursementDate(final LoanCharge loanCharge) {
Date actualDisbursementDate = this.actualDisbursementDate;
if (loanCharge.isDueAtDisbursement() && loanCharge.isActive()) {
LoanTrancheDisbursementCharge trancheDisbursementCharge = loanCharge.getTrancheDisbursementCharge();
if (trancheDisbursementCharge != null) {
LoanDisbursementDetails details = trancheDisbursementCharge.getloanDisbursementDetails();
actualDisbursementDate = details.actualDisbursementDate();
}
}
return actualDisbursementDate;
}
public void addTrancheLoanCharge(final Charge charge) {
final List<Charge> appliedCharges = new ArrayList<>();
for (final LoanTrancheCharge loanTrancheCharge : this.trancheCharges) {
appliedCharges.add(loanTrancheCharge.getCharge());
}
if (!appliedCharges.contains(charge)) {
this.trancheCharges.add(new LoanTrancheCharge(charge, this));
}
}
public Map<String, Object> undoLastDisbursal(ScheduleGeneratorDTO scheduleGeneratorDTO, List<Long> existingTransactionIds,
List<Long> existingReversedTransactionIds, AppUser currentUser, Loan loan) {
validateAccountStatus(LoanEvent.LOAN_DISBURSAL_UNDO_LAST);
existingTransactionIds.addAll(findExistingTransactionIds());
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final Map<String, Object> actualChanges = new LinkedHashMap<>();
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_DISBURSAL_UNDO_LAST, getDisbursementDate());
LocalDate actualDisbursementDate = null;
LocalDate lastTransactionDate = getDisbursementDate();
List<LoanTransaction> loanTransactions = retreiveListOfTransactionsExcludeAccruals();
Collections.reverse(loanTransactions);
for (final LoanTransaction previousTransaction : loanTransactions) {
if (lastTransactionDate.isBefore(previousTransaction.getTransactionDate())) {
if (previousTransaction.isRepayment() || previousTransaction.isWaiver() || previousTransaction.isChargePayment()) {
throw new UndoLastTrancheDisbursementException(previousTransaction.getId());
}
}
if (previousTransaction.isDisbursement()) {
lastTransactionDate = previousTransaction.getTransactionDate();
break;
}
}
actualDisbursementDate = lastTransactionDate;
updateLoanToLastDisbursalState(actualDisbursementDate);
for (Iterator<LoanTermVariations> iterator = this.loanTermVariations.iterator(); iterator.hasNext();) {
LoanTermVariations loanTermVariations = iterator.next();
if ((loanTermVariations.getTermType().isDueDateVariation()
&& loanTermVariations.fetchDateValue().isAfter(actualDisbursementDate))
|| (loanTermVariations.getTermType().isEMIAmountVariation() && loanTermVariations.getTermApplicableFrom()
.compareTo(Date.from(actualDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant())) == 0
? Boolean.TRUE
: Boolean.FALSE)
|| loanTermVariations.getTermApplicableFrom()
.after(Date.from(actualDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant()))) {
iterator.remove();
}
}
reverseExistingTransactionsTillLastDisbursal(actualDisbursementDate);
loan.recalculateScheduleFromLastTransaction(scheduleGeneratorDTO, existingTransactionIds, existingReversedTransactionIds,
currentUser);
actualChanges.put("undolastdisbursal", "true");
actualChanges.put("disbursedAmount", this.getDisbursedAmount());
updateLoanSummaryDerivedFields();
return actualChanges;
}
/**
* Reverse only disbursement, accruals, and repayments at disbursal transactions
*
* @param actualDisbursementDate
* @return
*/
public List<LoanTransaction> reverseExistingTransactionsTillLastDisbursal(LocalDate actualDisbursementDate) {
final List<LoanTransaction> reversedTransactions = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if ((actualDisbursementDate.equals(transaction.getTransactionDate())
|| actualDisbursementDate.isBefore(transaction.getTransactionDate()))
&& transaction.isAllowTypeTransactionAtTheTimeOfLastUndo()) {
reversedTransactions.add(transaction);
transaction.reverse();
}
}
return reversedTransactions;
}
private void updateLoanToLastDisbursalState(LocalDate actualDisbursementDate) {
for (final LoanCharge charge : charges()) {
if (charge.isOverdueInstallmentCharge()) {
charge.setActive(false);
} else if (charge.isTrancheDisbursementCharge() && actualDisbursementDate.equals(LocalDate.ofInstant(
charge.getTrancheDisbursementCharge().getloanDisbursementDetails().actualDisbursementDate().toInstant(),
DateUtils.getDateTimeZoneOfTenant()))) {
charge.resetToOriginal(loanCurrency());
}
}
for (final LoanDisbursementDetails details : this.disbursementDetails) {
if (actualDisbursementDate
.equals(LocalDate.ofInstant(details.actualDisbursementDate().toInstant(), DateUtils.getDateTimeZoneOfTenant()))) {
this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount().subtract(details.principal()));
details.updateActualDisbursementDate(null);
}
}
updateLoanSummaryDerivedFields();
}
public Boolean getIsFloatingInterestRate() {
return this.isFloatingInterestRate;
}
public BigDecimal getInterestRateDifferential() {
return this.interestRateDifferential;
}
public void setIsFloatingInterestRate(Boolean isFloatingInterestRate) {
this.isFloatingInterestRate = isFloatingInterestRate;
}
public void setInterestRateDifferential(BigDecimal interestRateDifferential) {
this.interestRateDifferential = interestRateDifferential;
}
public List<LoanTermVariations> getLoanTermVariations() {
return this.loanTermVariations;
}
private int adjustNumberOfRepayments() {
int repaymetsForAdjust = 0;
for (LoanTermVariations loanTermVariations : this.loanTermVariations) {
if (loanTermVariations.getTermType().isInsertInstallment()) {
repaymetsForAdjust++;
} else if (loanTermVariations.getTermType().isDeleteInstallment()) {
repaymetsForAdjust--;
}
}
return repaymetsForAdjust;
}
public int fetchNumberOfInstallmensAfterExceptions() {
if (this.repaymentScheduleInstallments.size() > 0) {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
int numberOfInstallments = 0;
for (final LoanRepaymentScheduleInstallment installment : installments) {
if (!installment.isRecalculatedInterestComponent()) {
numberOfInstallments++;
}
}
return numberOfInstallments;
}
return this.repaymentScheduleDetail().getNumberOfRepayments() + adjustNumberOfRepayments();
}
public void setExpectedFirstRepaymentOnDate(Date expectedFirstRepaymentOnDate) {
this.expectedFirstRepaymentOnDate = expectedFirstRepaymentOnDate;
}
/*
* get the next repayment date for rescheduling at the time of disbursement
*/
public LocalDate getNextPossibleRepaymentDateForRescheduling() {
List<LoanDisbursementDetails> loanDisbursementDetails = this.disbursementDetails;
LocalDate nextRepaymentDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
for (LoanDisbursementDetails loanDisbursementDetail : loanDisbursementDetails) {
if (loanDisbursementDetail.actualDisbursementDate() == null) {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getDueDate().isEqual(loanDisbursementDetail.expectedDisbursementDateAsLocalDate())
|| (installment.getDueDate().isAfter(loanDisbursementDetail.expectedDisbursementDateAsLocalDate())
&& installment.isNotFullyPaidOff())) {
nextRepaymentDate = installment.getDueDate();
break;
}
}
break;
}
}
return nextRepaymentDate;
}
public BigDecimal getDerivedAmountForCharge(final LoanCharge loanCharge) {
BigDecimal amount = BigDecimal.ZERO;
if (isMultiDisburmentLoan() && loanCharge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue())) {
amount = getApprovedPrincipal();
} else {
// If charge type is specified due date and loan is multi disburment
// loan.
// Then we need to get as of this loan charge due date how much
// amount disbursed.
if (loanCharge.isSpecifiedDueDate() && this.isMultiDisburmentLoan()) {
for (final LoanDisbursementDetails loanDisbursementDetails : this.getDisbursementDetails()) {
if (!loanDisbursementDetails.expectedDisbursementDate().after(loanCharge.getDueDate())) {
amount = amount.add(loanDisbursementDetails.principal());
}
}
} else {
amount = getPrincpal().getAmount();
}
}
return amount;
}
public void updatePostDatedChecks(final List<PostDatedChecks> postDatedChecks) {
this.postDatedChecks = postDatedChecks;
}
public List<PostDatedChecks> getPostDatedChecks() {
return this.postDatedChecks;
}
public void updateWriteOffReason(CodeValue writeOffReason) {
this.writeOffReason = writeOffReason;
}
public Group getGroup() {
return group;
}
public LoanProduct getLoanProduct() {
return loanProduct;
}
public LoanRepaymentScheduleInstallment fetchLoanForeclosureDetail(final LocalDate closureDate) {
Money[] receivables = retriveIncomeOutstandingTillDate(closureDate);
Money totalPrincipal = Money.of(getCurrency(), this.getSummary().getTotalPrincipalOutstanding());
totalPrincipal = totalPrincipal.minus(receivables[3]);
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null;
final LocalDate currentDate = DateUtils.getLocalDateOfTenant();
return new LoanRepaymentScheduleInstallment(null, 0, currentDate, currentDate, totalPrincipal.getAmount(),
receivables[0].getAmount(), receivables[1].getAmount(), receivables[2].getAmount(), false, compoundingDetails);
}
public Money[] retriveIncomeOutstandingTillDate(final LocalDate paymentDate) {
Money[] balances = new Money[4];
final MonetaryCurrency currency = getCurrency();
Money interest = Money.zero(currency);
Money paidFromFutureInstallments = Money.zero(currency);
Money fee = Money.zero(currency);
Money penalty = Money.zero(currency);
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
if (!installment.getDueDate().isAfter(paymentDate)) {
interest = interest.plus(installment.getInterestOutstanding(currency));
penalty = penalty.plus(installment.getPenaltyChargesOutstanding(currency));
fee = fee.plus(installment.getFeeChargesOutstanding(currency));
} else if (installment.getFromDate().isBefore(paymentDate)) {
Money[] balancesForCurrentPeroid = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment);
if (balancesForCurrentPeroid[0].isGreaterThan(balancesForCurrentPeroid[5])) {
interest = interest.plus(balancesForCurrentPeroid[0]).minus(balancesForCurrentPeroid[5]);
} else {
paidFromFutureInstallments = paidFromFutureInstallments.plus(balancesForCurrentPeroid[5])
.minus(balancesForCurrentPeroid[0]);
}
if (balancesForCurrentPeroid[1].isGreaterThan(balancesForCurrentPeroid[3])) {
fee = fee.plus(balancesForCurrentPeroid[1].minus(balancesForCurrentPeroid[3]));
} else {
paidFromFutureInstallments = paidFromFutureInstallments
.plus(balancesForCurrentPeroid[3].minus(balancesForCurrentPeroid[1]));
}
if (balancesForCurrentPeroid[2].isGreaterThan(balancesForCurrentPeroid[4])) {
penalty = penalty.plus(balancesForCurrentPeroid[2].minus(balancesForCurrentPeroid[4]));
} else {
paidFromFutureInstallments = paidFromFutureInstallments.plus(balancesForCurrentPeroid[4])
.minus(balancesForCurrentPeroid[2]);
}
} else if (installment.getDueDate().isAfter(paymentDate)) {
paidFromFutureInstallments = paidFromFutureInstallments.plus(installment.getInterestPaid(currency))
.plus(installment.getPenaltyChargesPaid(currency)).plus(installment.getFeeChargesPaid(currency));
}
}
balances[0] = interest;
balances[1] = fee;
balances[2] = penalty;
balances[3] = paidFromFutureInstallments;
return balances;
}
private Money[] fetchInterestFeeAndPenaltyTillDate(final LocalDate paymentDate, final MonetaryCurrency currency,
final LoanRepaymentScheduleInstallment installment) {
Money penaltyForCurrentPeriod = Money.zero(getCurrency());
Money penaltyAccoutedForCurrentPeriod = Money.zero(getCurrency());
Money feeForCurrentPeriod = Money.zero(getCurrency());
Money feeAccountedForCurrentPeriod = Money.zero(getCurrency());
Money interestForCurrentPeriod = Money.zero(getCurrency());
Money interestAccountedForCurrentPeriod = Money.zero(getCurrency());
int totalPeriodDays = Math.toIntExact(ChronoUnit.DAYS.between(installment.getFromDate(), installment.getDueDate()));
int tillDays = Math.toIntExact(ChronoUnit.DAYS.between(installment.getFromDate(), paymentDate));
interestForCurrentPeriod = Money.of(getCurrency(), BigDecimal
.valueOf(calculateInterestForDays(totalPeriodDays, installment.getInterestCharged(getCurrency()).getAmount(), tillDays)));
interestAccountedForCurrentPeriod = installment.getInterestWaived(getCurrency()).plus(installment.getInterestPaid(getCurrency()));
for (LoanCharge loanCharge : this.charges) {
if (loanCharge.isActive() && !loanCharge.isDueAtDisbursement()) {
if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), paymentDate)) {
if (loanCharge.isPenaltyCharge()) {
penaltyForCurrentPeriod = penaltyForCurrentPeriod.plus(loanCharge.getAmount(getCurrency()));
penaltyAccoutedForCurrentPeriod = penaltyAccoutedForCurrentPeriod
.plus(loanCharge.getAmountWaived(getCurrency()).plus(loanCharge.getAmountPaid(getCurrency())));
} else {
feeForCurrentPeriod = feeForCurrentPeriod.plus(loanCharge.getAmount(currency));
feeAccountedForCurrentPeriod = feeAccountedForCurrentPeriod.plus(loanCharge.getAmountWaived(getCurrency()).plus(
loanCharge.getAmountPaid(getCurrency())));
}
} else if (loanCharge.isInstalmentFee()) {
LoanInstallmentCharge loanInstallmentCharge = loanCharge.getInstallmentLoanCharge(installment.getInstallmentNumber());
if (loanCharge.isPenaltyCharge()) {
penaltyAccoutedForCurrentPeriod = penaltyAccoutedForCurrentPeriod
.plus(loanInstallmentCharge.getAmountPaid(currency));
} else {
feeAccountedForCurrentPeriod = feeAccountedForCurrentPeriod.plus(loanInstallmentCharge.getAmountPaid(currency));
}
}
}
}
Money[] balances = new Money[6];
balances[0] = interestForCurrentPeriod;
balances[1] = feeForCurrentPeriod;
balances[2] = penaltyForCurrentPeriod;
balances[3] = feeAccountedForCurrentPeriod;
balances[4] = penaltyAccoutedForCurrentPeriod;
balances[5] = interestAccountedForCurrentPeriod;
return balances;
}
public Money[] retriveIncomeForOverlappingPeriod(final LocalDate paymentDate) {
Money[] balances = new Money[3];
final MonetaryCurrency currency = getCurrency();
balances[0] = balances[1] = balances[2] = Money.zero(currency);
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
if (installment.getDueDate().isEqual(paymentDate)) {
Money interest = installment.getInterestCharged(currency);
Money fee = installment.getFeeChargesCharged(currency);
Money penalty = installment.getPenaltyChargesCharged(currency);
balances[0] = interest;
balances[1] = fee;
balances[2] = penalty;
break;
} else if (installment.getDueDate().isAfter(paymentDate) && installment.getFromDate().isBefore(paymentDate)) {
balances = fetchInterestFeeAndPenaltyTillDate(paymentDate, currency, installment);
break;
}
}
return balances;
}
private double calculateInterestForDays(int daysInPeriod, BigDecimal interest, int days) {
if (interest.doubleValue() == 0) {
return 0;
}
return interest.doubleValue() / daysInPeriod * days;
}
public Money[] getReceivableIncome(final LocalDate tillDate) {
MonetaryCurrency currency = getCurrency();
Money receivableInterest = Money.zero(currency);
Money receivableFee = Money.zero(currency);
Money receivablePenalty = Money.zero(currency);
Money[] receivables = new Money[3];
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement()
&& !transaction.getTransactionDate().isAfter(tillDate)) {
if (transaction.isAccrual()) {
receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency));
receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency));
receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(currency));
} else if (transaction.isRepayment() || transaction.isChargePayment()) {
receivableInterest = receivableInterest.minus(transaction.getInterestPortion(currency));
receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency));
receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency));
}
}
if (receivableInterest.isLessThanZero()) {
receivableInterest = receivableInterest.zero();
}
if (receivableFee.isLessThanZero()) {
receivableFee = receivableFee.zero();
}
if (receivablePenalty.isLessThanZero()) {
receivablePenalty = receivablePenalty.zero();
}
}
receivables[0] = receivableInterest;
receivables[1] = receivableFee;
receivables[2] = receivablePenalty;
return receivables;
}
public void reverseAccrualsAfter(final LocalDate tillDate) {
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isAccrual() && transaction.getTransactionDate().isAfter(tillDate)) {
transaction.reverse();
}
}
}
public ChangedTransactionDetail handleForeClosureTransactions(final LoanTransaction repaymentTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final ScheduleGeneratorDTO scheduleGeneratorDTO,
final AppUser appUser) {
LoanEvent event = LoanEvent.LOAN_FORECLOSURE;
validateAccountStatus(event);
validateForForeclosure(repaymentTransaction.getTransactionDate());
this.loanSubStatus = LoanSubStatus.FORECLOSED.getValue();
applyAccurals(appUser);
return handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction, loanLifecycleStateMachine, null, scheduleGeneratorDTO,
appUser);
}
public Money retrieveAccruedAmountAfterDate(final LocalDate tillDate) {
Money totalAmountAccrued = Money.zero(getCurrency());
Money actualAmountTobeAccrued = Money.zero(getCurrency());
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
totalAmountAccrued = totalAmountAccrued.plus(installment.getInterestAccrued(getCurrency()));
if (tillDate.isAfter(installment.getFromDate()) && tillDate.isBefore(installment.getDueDate())) {
int daysInPeriod = Math.toIntExact(ChronoUnit.DAYS.between(installment.getFromDate(), installment.getDueDate()));
int tillDays = Math.toIntExact(ChronoUnit.DAYS.between(installment.getFromDate(), tillDate));
double interest = calculateInterestForDays(daysInPeriod, installment.getInterestCharged(getCurrency()).getAmount(),
tillDays);
actualAmountTobeAccrued = actualAmountTobeAccrued.plus(interest);
} else if ((tillDate.isAfter(installment.getFromDate()) && tillDate.isEqual(installment.getDueDate()))
|| (tillDate.isEqual(installment.getFromDate()) && tillDate.isEqual(installment.getDueDate()))
|| (tillDate.isAfter(installment.getFromDate()) && tillDate.isAfter(installment.getDueDate()))) {
actualAmountTobeAccrued = actualAmountTobeAccrued.plus(installment.getInterestAccrued(getCurrency()));
}
}
Money accredAmountAfterDate = totalAmountAccrued.minus(actualAmountTobeAccrued);
if (accredAmountAfterDate.isLessThanZero()) {
accredAmountAfterDate = Money.zero(getCurrency());
}
return accredAmountAfterDate;
}
public void validateForForeclosure(final LocalDate transactionDate) {
if (isInterestRecalculationEnabledForProduct()) {
final String defaultUserMessage = "The loan with interest recalculation enabled cannot be foreclosed.";
throw new LoanForeclosureException("loan.with.interest.recalculation.enabled.cannot.be.foreclosured", defaultUserMessage,
getId());
}
LocalDate lastUserTransactionDate = getLastUserTransactionDate();
if (DateUtils.isDateInTheFuture(transactionDate)) {
final String defaultUserMessage = "The transactionDate cannot be in the future.";
throw new LoanForeclosureException("loan.foreclosure.transaction.date.is.in.future", defaultUserMessage, transactionDate);
}
if (lastUserTransactionDate.isAfter(transactionDate)) {
final String defaultUserMessage = "The transactionDate cannot be in the future.";
throw new LoanForeclosureException("loan.foreclosure.transaction.date.cannot.before.the.last.transaction.date",
defaultUserMessage, transactionDate);
}
}
public void updateInstallmentsPostDate(LocalDate transactionDate) {
List<LoanRepaymentScheduleInstallment> newInstallments = new ArrayList<>(this.repaymentScheduleInstallments);
final MonetaryCurrency currency = getCurrency();
Money totalPrincipal = Money.zero(currency);
Money[] balances = retriveIncomeForOverlappingPeriod(transactionDate);
boolean isInterestComponent = true;
for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
if (!installment.getDueDate().isBefore(transactionDate)) {
totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency));
newInstallments.remove(installment);
if (installment.getDueDate().isEqual(transactionDate)) {
isInterestComponent = false;
}
}
}
for (LoanDisbursementDetails loanDisbursementDetails : getDisbursementDetails()) {
if (loanDisbursementDetails.actualDisbursementDate() == null) {
totalPrincipal = Money.of(currency, totalPrincipal.getAmount().subtract(loanDisbursementDetails.principal()));
}
}
LocalDate installmentStartDate = getDisbursementDate();
if (newInstallments.size() > 0) {
installmentStartDate = newInstallments.get(newInstallments.size() - 1).getDueDate();
}
int installmentNumber = newInstallments.size();
if (!isInterestComponent) {
installmentNumber++;
}
LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(null, newInstallments.size() + 1,
installmentStartDate, transactionDate, totalPrincipal.getAmount(), balances[0].getAmount(), balances[1].getAmount(),
balances[2].getAmount(), isInterestComponent, null);
newInstallment.updateInstallmentNumber(newInstallments.size() + 1);
newInstallments.add(newInstallment);
updateLoanScheduleOnForeclosure(newInstallments);
Set<LoanCharge> charges = this.charges();
int penaltyWaitPeriod = 0;
for (LoanCharge loanCharge : charges) {
if (loanCharge.getDueLocalDate() != null && loanCharge.getDueLocalDate().isAfter(transactionDate)) {
loanCharge.setActive(false);
} else if (loanCharge.getDueLocalDate() == null) {
recalculateLoanCharge(loanCharge, penaltyWaitPeriod);
loanCharge.updateWaivedAmount(currency);
}
}
for (LoanTransaction loanTransaction : getLoanTransactions()) {
if (loanTransaction.isChargesWaiver()) {
for (LoanChargePaidBy chargePaidBy : loanTransaction.getLoanChargesPaid()) {
if ((chargePaidBy.getLoanCharge().isDueDateCharge()
&& chargePaidBy.getLoanCharge().getDueLocalDate().isAfter(transactionDate))
|| (chargePaidBy.getLoanCharge().isInstalmentFee() && chargePaidBy.getInstallmentNumber() != null
&& chargePaidBy.getInstallmentNumber() > installmentNumber)) {
loanTransaction.reverse();
}
}
}
}
}
public void updateLoanScheduleOnForeclosure(final Collection<LoanRepaymentScheduleInstallment> installments) {
this.repaymentScheduleInstallments.clear();
for (final LoanRepaymentScheduleInstallment installment : installments) {
addLoanRepaymentScheduleInstallment(installment);
}
}
public Integer getLoanSubStatus() {
return this.loanSubStatus;
}
private boolean isForeclosure() {
boolean isForeClosure = false;
if (this.loanSubStatus != null) {
isForeClosure = LoanSubStatus.fromInt(loanSubStatus).isForeclosed();
}
return isForeClosure;
}
public Set<LoanTermVariations> getActiveLoanTermVariations() {
Set<LoanTermVariations> retData = new HashSet<>();
if (this.loanTermVariations != null && this.loanTermVariations.size() > 0) {
for (LoanTermVariations loanTermVariations : this.loanTermVariations) {
if (loanTermVariations.isActive()) {
retData.add(loanTermVariations);
}
}
}
return retData.size() > 0 ? retData : null;
}
public void setIsTopup(final boolean isTopup) {
this.isTopup = isTopup;
}
public boolean isTopup() {
return this.isTopup;
}
public BigDecimal getFirstDisbursalAmount() {
BigDecimal firstDisbursalAmount;
if (this.isMultiDisburmentLoan()) {
List<DisbursementData> disbursementData = getDisbursmentData();
Collections.sort(disbursementData);
firstDisbursalAmount = disbursementData.get(disbursementData.size() - 1).amount();
} else {
firstDisbursalAmount = this.getLoanRepaymentScheduleDetail().getPrincipal().getAmount();
}
return firstDisbursalAmount;
}
public void setTopupLoanDetails(LoanTopupDetails topupLoanDetails) {
this.loanTopupDetails = topupLoanDetails;
}
public LoanTopupDetails getTopupLoanDetails() {
return this.loanTopupDetails;
}
public Collection<LoanCharge> getLoanCharges() {
return this.charges;
}
public void initializeLazyCollections() {
checkAndFetchLazyCollection(this.charges);
checkAndFetchLazyCollection(this.trancheCharges);
checkAndFetchLazyCollection(this.repaymentScheduleInstallments);
checkAndFetchLazyCollection(this.loanTransactions);
checkAndFetchLazyCollection(this.disbursementDetails);
checkAndFetchLazyCollection(this.loanTermVariations);
checkAndFetchLazyCollection(this.collateral);
checkAndFetchLazyCollection(this.loanOfficerHistory);
checkAndFetchLazyCollection(this.loanCollateralManagements);
}
private void checkAndFetchLazyCollection(Collection lazyCollection) {
if (lazyCollection != null) {
lazyCollection.size();
}
}
public void initializeLoanOfficerHistory() {
this.loanOfficerHistory.size();
}
public void initilizeTransactions() {
this.loanTransactions.size();
}
public void initializeRepaymentSchedule() {
this.repaymentScheduleInstallments.size();
}
public boolean hasInvalidLoanType() {
return AccountType.fromInt(this.loanType).isInvalid();
}
public boolean isIndividualLoan() {
return AccountType.fromInt(this.loanType).isIndividualAccount();
}
public void setRates(List<Rate> rates) {
this.rates = rates;
}
public List<Rate> getRates() {
return rates;
}
public Integer getLoanType() {
return loanType;
}
public void setLoanType(Integer loanType) {
this.loanType = loanType;
}
public Set<LoanCollateralManagement> getLoanCollateralManagements() {
return this.loanCollateralManagements;
}
public void adjustNetDisbursalAmount(BigDecimal adjustedAmount) {
this.netDisbursalAmount = adjustedAmount.subtract(this.deriveSumTotalOfChargesDueAtDisbursement());
}
public CreditScorecard getScorecard() {
return scorecard;
}
public boolean hasScorecard() {
return this.scorecard != null;
}
}