FINERACT-2080: Extract DTO mappers from loan entity

This commit is contained in:
Oleksii Novikov
2025-04-07 13:13:35 +02:00
committed by Adam Saghy
parent b01dca3ecc
commit 2ba7e28108
28 changed files with 995 additions and 776 deletions
@@ -0,0 +1,49 @@
/**
* 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.data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AccountingBridgeDataDTO {
private Long loanId;
private Long loanProductId;
private Long officeId;
private String currencyCode;
private BigDecimal calculatedInterest;
private boolean cashBasedAccountingEnabled;
private boolean upfrontAccrualBasedAccountingEnabled;
private boolean periodicAccrualBasedAccountingEnabled;
private boolean isAccountTransfer;
private boolean isChargeOff;
private boolean isFraud;
private Long chargeOffReasonCodeValue;
private List<AccountingBridgeLoanTransactionDTO> newLoanTransactions = new ArrayList<>();
}
@@ -0,0 +1,56 @@
/**
* 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.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AccountingBridgeLoanTransactionDTO {
private Long id;
private Long officeId;
private LoanTransactionEnumData type;
private boolean reversed;
private LocalDate date;
private String currencyCode;
private BigDecimal amount;
private BigDecimal netDisbursalAmount;
private BigDecimal principalPortion;
private BigDecimal interestPortion;
private BigDecimal feeChargesPortion;
private BigDecimal penaltyChargesPortion;
private BigDecimal overPaymentPortion;
private String chargeRefundChargeType;
private Long paymentTypeId;
private List<LoanChargePaidByDTO> loanChargesPaid = new ArrayList<>();
private BigDecimal principalPaid;
private BigDecimal feePaid;
private BigDecimal penaltyPaid;
private LoanChargeData loanChargeData;
private boolean loanToLoanTransfer;
}
@@ -0,0 +1,37 @@
/**
* 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.data;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoanChargePaidByDTO {
private Long chargeId;
private Boolean isPenalty;
private Long loanChargeId;
private BigDecimal amount;
private Integer installmentNumber;
}
@@ -21,9 +21,6 @@ package org.apache.fineract.portfolio.loanaccount.domain;
import static org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType.CUMULATIVE;
import static org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType.PROGRESSIVE;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
@@ -43,20 +40,16 @@ import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
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.Optional;
import java.util.Set;
import java.util.function.Predicate;
@@ -67,61 +60,36 @@ import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
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.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.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.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.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.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
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.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
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.LoanSupportedInterestRefundTypes;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.portfolio.rate.domain.Rate;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
@@ -186,7 +154,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@JoinColumn(name = "product_id", nullable = false)
private LoanProduct loanProduct;
@ManyToOne(optional = true, fetch = FetchType.EAGER)
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "fund_id")
private Fund fund;
@@ -246,7 +214,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
private LocalDate rejectedOnDate;
@Setter()
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rejectedon_userid")
private AppUser rejectedBy;
@@ -255,7 +223,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
private LocalDate withdrawnOnDate;
@Setter()
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "withdrawnon_userid")
private AppUser withdrawnBy;
@@ -264,7 +232,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
private LocalDate approvedOnDate;
@Setter()
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "approvedon_userid")
private AppUser approvedBy;
@@ -277,7 +245,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
private LocalDate actualDisbursementDate;
@Setter()
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "disbursedon_userid")
private AppUser disbursedBy;
@@ -286,7 +254,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
private LocalDate closedOnDate;
@Setter()
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "closedon_userid")
private AppUser closedBy;
@@ -298,7 +266,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "rescheduledon_date")
private LocalDate rescheduledOnDate;
@ManyToOne(optional = true, fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "rescheduledon_userid")
private AppUser rescheduledByUser;
@@ -396,7 +364,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@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)
@OneToOne(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private LoanInterestRecalculationDetails loanInterestRecalculationDetails;
@Column(name = "is_npa", nullable = false)
@@ -439,7 +407,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "is_fraud", nullable = false)
private boolean fraud = false;
@OneToOne(cascade = CascadeType.ALL, mappedBy = "loan", optional = true, orphanRemoval = true, fetch = FetchType.LAZY)
@OneToOne(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private LoanTopupDetails loanTopupDetails;
@OneToMany(fetch = FetchType.LAZY)
@@ -709,7 +677,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
* 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
* is lesser than today's date. If not, the transaction date is set to today's date
*/
public LoanTransaction handleChargeAppliedTransaction(final LoanCharge loanCharge, final LocalDate suppliedTransactionDate) {
if (isProgressiveSchedule()) {
@@ -782,9 +750,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
if (transaction.isGreaterThan(chargeAmount)) {
final Money principalPortion = Money.zero(currency);
final Money interestPortion = Money.zero(currency);
final Money penaltychargesPortion = Money.zero(currency);
final Money penaltyChargesPortion = Money.zero(currency);
transaction.updateComponentsAndTotal(principalPortion, interestPortion, chargeAmount, penaltychargesPortion);
transaction.updateComponentsAndTotal(principalPortion, interestPortion, chargeAmount, penaltyChargesPortion);
} else {
transactionToRemove = transaction;
@@ -874,12 +842,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.summary.getTotalWrittenOff();
}
/**
* @param calculationType
* @param percentage
* @param installment
* @return
*/
private Money calculateInstallmentChargeAmount(final ChargeCalculationType calculationType, final BigDecimal percentage,
final LoanRepaymentScheduleInstallment installment) {
Money percentOf = switch (calculationType) {
@@ -1113,64 +1075,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
};
}
// This method returns date format and locale if present in the JsonCommand
public 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;
}
public Map<String, Object> parseDisbursementDetails(final JsonObject jsonObject, String dateFormat, Locale locale) {
Map<String, Object> returnObject = new HashMap<>();
if (jsonObject.get(LoanApiConstants.expectedDisbursementDateParameterName) != null
&& jsonObject.get(LoanApiConstants.expectedDisbursementDateParameterName).isJsonPrimitive()) {
final JsonPrimitive primitive = jsonObject.get(LoanApiConstants.expectedDisbursementDateParameterName).getAsJsonPrimitive();
final String valueAsString = primitive.getAsString();
if (StringUtils.isNotBlank(valueAsString)) {
LocalDate date = JsonParserHelper.convertFrom(valueAsString, LoanApiConstants.expectedDisbursementDateParameterName,
dateFormat, locale);
if (date != null) {
returnObject.put(LoanApiConstants.expectedDisbursementDateParameterName, date);
}
}
}
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 List<Long> fetchLoanTrancheChargeIds() {
return getCharges().stream()//
.filter(charge -> charge.isTrancheDisbursementCharge() && charge.isActive()) //
@@ -1344,56 +1248,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
}
public LoanScheduleModel regenerateScheduleModel(final ScheduleGeneratorDTO scheduleGeneratorDTO) {
final MathContext mc = MoneyHelper.getMathContext();
final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(scheduleGeneratorDTO);
LoanScheduleGenerator loanScheduleGenerator;
if (loanApplicationTerms.isEqualAmortization()) {
if (loanApplicationTerms.getInterestMethod().isDecliningBalance()) {
final LoanScheduleGenerator decliningLoanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory()
.create(loanApplicationTerms.getLoanScheduleType(), InterestMethod.DECLINING_BALANCE);
Set<LoanCharge> loanCharges = getActiveCharges();
LoanScheduleModel loanSchedule = decliningLoanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges,
scheduleGeneratorDTO.getHolidayDetailDTO());
loanApplicationTerms
.updateTotalInterestDue(Money.of(loanApplicationTerms.getCurrency(), loanSchedule.getTotalInterestCharged()));
}
loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(loanApplicationTerms.getLoanScheduleType(),
InterestMethod.FLAT);
} else {
loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(loanApplicationTerms.getLoanScheduleType(),
loanApplicationTerms.getInterestMethod());
}
return loanScheduleGenerator.generate(mc, loanApplicationTerms, getActiveCharges(), scheduleGeneratorDTO.getHolidayDetailDTO());
}
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.getBusinessLocalDate();
for (FloatingRatePeriodData periodData : applicableRates) {
LoanTermVariationsData loanTermVariation = new LoanTermVariationsData(
LoanEnumerations.loanVariationType(LoanTermVariationType.INTEREST_RATE), periodData.getFromDateAsLocalDate(),
periodData.getInterestRate(), dateValue, isSpecificToInstallment);
if (!DateUtils.isBefore(interestRateStartDate, periodData.getFromDateAsLocalDate())) {
interestRateStartDate = periodData.getFromDateAsLocalDate();
interestRate = periodData.getInterestRate();
}
loanTermVariations.add(loanTermVariation);
}
}
return interestRate;
}
public boolean isAutoRepaymentForDownPaymentEnabled() {
return this.loanRepaymentScheduleDetail.isEnableDownPayment()
&& this.loanRepaymentScheduleDetail.isEnableAutoRepaymentForDownPayment();
@@ -1664,7 +1518,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
}
// if total paid in transactions doesnt match repayment schedule then there's an overpayment.
// if total paid in transactions doesn't match repayment schedule then there's an overpayment.
return totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments);
}
@@ -1945,7 +1799,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.client != null ? this.client.getOffice() : this.group.getOffice();
}
private Boolean isCashBasedAccountingEnabledOnLoanProduct() {
public Boolean isCashBasedAccountingEnabledOnLoanProduct() {
return this.loanProduct.isCashBasedAccountingEnabled();
}
@@ -1970,165 +1824,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.loanProduct.getId();
}
public List<Map<String, Object>> deriveAccountingBridgeDataForChargeOff(final String currencyCode,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, boolean isAccountTransfer) {
final List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
final List<Map<String, Object>> newLoanTransactionsBeforeChargeOff = new ArrayList<>();
final List<Map<String, Object>> newLoanTransactionsAfterChargeOff = new ArrayList<>();
// get map before charge-off
final Map<String, Object> accountingBridgeDataBeforeChargeOff = buildAccountingMapForChargeOffDateCriteria(currencyCode,
isAccountTransfer, true);
// get map after charge-off
final Map<String, Object> accountingBridgeDataAfterChargeOff = buildAccountingMapForChargeOffDateCriteria(currencyCode,
isAccountTransfer, false);
// split the transactions according charge-off date
classifyTransactionsBasedOnChargeOffDate(newLoanTransactionsBeforeChargeOff, newLoanTransactionsAfterChargeOff,
existingTransactionIds, existingReversedTransactionIds, currencyCode);
accountingBridgeDataBeforeChargeOff.put("newLoanTransactions", newLoanTransactionsBeforeChargeOff);
accountingBridgeData.add(accountingBridgeDataBeforeChargeOff);
accountingBridgeDataAfterChargeOff.put("newLoanTransactions", newLoanTransactionsAfterChargeOff);
accountingBridgeData.add(accountingBridgeDataAfterChargeOff);
return accountingBridgeData;
}
private void classifyTransactionsBasedOnChargeOffDate(List<Map<String, Object>> newLoanTransactionsBeforeChargeOff,
List<Map<String, Object>> newLoanTransactionsAfterChargeOff, List<Long> existingTransactionIds,
List<Long> existingReversedTransactionIds, String currencyCode) {
// Before
filterTransactionsByChargeOffDate(newLoanTransactionsBeforeChargeOff, currencyCode, existingTransactionIds,
existingReversedTransactionIds, transaction -> DateUtils.isBefore(transaction.getTransactionDate(), getChargedOffOnDate()));
// On
filterTransactionsByChargeOffDate(newLoanTransactionsBeforeChargeOff, newLoanTransactionsAfterChargeOff, currencyCode,
existingTransactionIds, existingReversedTransactionIds,
transaction -> DateUtils.isEqual(transaction.getTransactionDate(), getChargedOffOnDate()));
// After
filterTransactionsByChargeOffDate(newLoanTransactionsAfterChargeOff, currencyCode, existingTransactionIds,
existingReversedTransactionIds, transaction -> DateUtils.isAfter(transaction.getTransactionDate(), getChargedOffOnDate()));
}
private Map<String, Object> getAccountingBridgeDataGenericAttributes(final String currencyCode, boolean isAccountTransfer) {
final Map<String, Object> accountingBridgeDataGenericAttributes = new LinkedHashMap<>();
accountingBridgeDataGenericAttributes.put("loanId", getId());
accountingBridgeDataGenericAttributes.put("loanProductId", productId());
accountingBridgeDataGenericAttributes.put("officeId", getOfficeId());
accountingBridgeDataGenericAttributes.put("currencyCode", currencyCode);
accountingBridgeDataGenericAttributes.put("calculatedInterest", this.summary.getTotalInterestCharged());
accountingBridgeDataGenericAttributes.put("cashBasedAccountingEnabled", isCashBasedAccountingEnabledOnLoanProduct());
accountingBridgeDataGenericAttributes.put("upfrontAccrualBasedAccountingEnabled", isUpfrontAccrualAccountingEnabledOnLoanProduct());
accountingBridgeDataGenericAttributes.put("periodicAccrualBasedAccountingEnabled",
isPeriodicAccrualAccountingEnabledOnLoanProduct());
accountingBridgeDataGenericAttributes.put("isAccountTransfer", isAccountTransfer);
return accountingBridgeDataGenericAttributes;
}
private Map<String, Object> buildAccountingMapForChargeOffDateCriteria(final String currencyCode, boolean isAccountTransfer,
boolean isBeforeChargeOffDate) {
final Map<String, Object> accountingBridgeDataChargeOff = new LinkedHashMap<>(
getAccountingBridgeDataGenericAttributes(currencyCode, isAccountTransfer));
accountingBridgeDataChargeOff.put("isChargeOff", !isBeforeChargeOffDate && isChargedOff());
accountingBridgeDataChargeOff.put("isFraud", isFraud());
accountingBridgeDataChargeOff.put("chargeOffReasonCodeValue", fetchChargeOffReasonId());
return accountingBridgeDataChargeOff;
}
private void filterTransactionsByChargeOffDate(List<Map<String, Object>> filteredTransactions, final String currencyCode,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
Predicate<LoanTransaction> chargeOffDateCriteria) {
filteredTransactions.addAll(this.loanTransactions.stream() //
.filter(chargeOffDateCriteria) //
.filter(transaction -> {
boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId());
boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId());
if (transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction) {
return true;
} else {
return !isExistingTransaction;
}
}) //
.map(transaction -> transaction.toMapData(currencyCode)).toList());
}
private void filterTransactionsByChargeOffDate(List<Map<String, Object>> newLoanTransactionsBeforeChargeOff,
List<Map<String, Object>> newLoanTransactionsAfterChargeOff, String currencyCode, List<Long> existingTransactionIds,
List<Long> existingReversedTransactionIds, Predicate<LoanTransaction> chargeOffDateCriteria) {
LoanTransaction chargeOffTransaction = this.loanTransactions.stream() //
.filter(LoanTransaction::isChargeOff) //
.filter(LoanTransaction::isNotReversed) //
.findFirst().get();
LoanTransaction originalChargeOffTransaction = getOriginalTransactionIfReverseReplayed(chargeOffTransaction);
this.loanTransactions.stream().filter(chargeOffDateCriteria).forEach(transaction -> {
boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId());
boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId());
List<Map<String, Object>> targetList = null;
if ((transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction)) {
// reversed transactions
LoanTransaction originalTransaction = getOriginalTransactionIfReverseReplayed(transaction);
targetList = originalTransaction.happenedBefore(originalChargeOffTransaction) ? newLoanTransactionsBeforeChargeOff
: newLoanTransactionsAfterChargeOff;
} else if (!isExistingTransaction) {
// new and replayed transactions
targetList = transaction.happenedBefore(chargeOffTransaction) ? newLoanTransactionsBeforeChargeOff
: newLoanTransactionsAfterChargeOff;
}
if (targetList != null) {
targetList.add(transaction.toMapData(currencyCode));
}
});
}
private LoanTransaction getOriginalTransactionIfReverseReplayed(LoanTransaction loanTransaction) {
if (!loanTransaction.getLoanTransactionRelations().isEmpty()) {
return loanTransaction.getLoanTransactionRelations().stream()
.filter(tr -> LoanTransactionRelationTypeEnum.REPLAYED.equals(tr.getRelationType())).map(tr -> tr.getToTransaction())
.collect(Collectors.toList()).stream().sorted(Comparator.comparingLong(LoanTransaction::getId)).findFirst()
.orElse(loanTransaction);
}
return loanTransaction;
}
public Map<String, Object> deriveAccountingBridgeData(final String currencyCode, 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("currencyCode", currencyCode);
accountingBridgeData.put("calculatedInterest", this.summary.getTotalInterestCharged());
accountingBridgeData.put("cashBasedAccountingEnabled", isCashBasedAccountingEnabledOnLoanProduct());
accountingBridgeData.put("upfrontAccrualBasedAccountingEnabled", isUpfrontAccrualAccountingEnabledOnLoanProduct());
accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", isPeriodicAccrualAccountingEnabledOnLoanProduct());
accountingBridgeData.put("isAccountTransfer", isAccountTransfer);
accountingBridgeData.put("isChargeOff", isChargedOff());
accountingBridgeData.put("chargeOffReasonCodeValue", fetchChargeOffReasonId());
accountingBridgeData.put("isFraud", isFraud());
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(currencyCode));
} else if (!existingTransactionIds.contains(transaction.getId())) {
newLoanTransactions.add(transaction.toMapData(currencyCode));
}
}
accountingBridgeData.put("newLoanTransactions", newLoanTransactions);
return accountingBridgeData;
}
private Long fetchChargeOffReasonId() {
public Long fetchChargeOffReasonId() {
return isChargedOff() && getChargeOffReason() != null ? getChargeOffReason().getId() : null;
}
@@ -2223,25 +1919,23 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final Integer loanRepaymentInterval = this.loanRepaymentScheduleDetail.getRepayEvery();
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
LocalDate newRepaymentDate = null;
LocalDate seedDate = meetingStartDate;
LocalDate newRepaymentDate;
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 (DateUtils.isAfter(oldDueDate, seedDate) && DateUtils.isDateInTheFuture(oldDueDate)) {
newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, seedDate, oldDueDate, loanRepaymentInterval,
frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays);
if (DateUtils.isAfter(oldDueDate, meetingStartDate) && DateUtils.isDateInTheFuture(oldDueDate)) {
newRepaymentDate = CalendarUtils.getNewRepaymentMeetingDate(recuringRule, meetingStartDate, oldDueDate,
loanRepaymentInterval, frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays);
final LocalDate maxDateLimitForNewRepayment = getMaxDateLimitForNewRepayment(repaymentPeriodFrequencyType,
loanRepaymentInterval, tmpFromDate);
if (DateUtils.isAfter(newRepaymentDate, maxDateLimitForNewRepayment)) {
newRepaymentDate = CalendarUtils.getNextRepaymentMeetingDate(recuringRule, seedDate, tmpFromDate, loanRepaymentInterval,
frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays);
newRepaymentDate = CalendarUtils.getNextRepaymentMeetingDate(recuringRule, meetingStartDate, tmpFromDate,
loanRepaymentInterval, frequency, workingDays, isSkipRepaymentonfirstdayofmonth, numberofDays);
}
if (isHolidayEnabled) {
@@ -2292,10 +1986,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public Integer getLoanProductLoanCounter() {
if (this.loanProductCounter == null) {
return 0;
}
return this.loanProductCounter;
return Objects.requireNonNullElse(this.loanProductCounter, 0);
}
public void updateClientLoanCounter(final Integer newLoanCounter) {
@@ -2510,76 +2201,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return (this.actualMaturityDate != null) ? (referenceDate.compareTo(this.actualMaturityDate) >= 0) : false;
}
public LoanApplicationTerms constructLoanApplicationTerms(final ScheduleGeneratorDTO scheduleGeneratorDTO) {
final Integer loanTermFrequency = this.termFrequency;
NthDayType nthDayType = null;
DayOfWeekType dayOfWeekType = null;
final List<DisbursementData> disbursementData = new ArrayList<>();
for (LoanDisbursementDetails disbursementDetails : getDisbursementDetails()) {
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;
RepaymentStartDateType repaymentStartDateType = this.getLoanProduct().getRepaymentStartDateType();
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();
}
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();
}
return LoanApplicationTerms.assembleFrom(scheduleGeneratorDTO.getCurrency(), loanTermFrequency, this.termPeriodFrequencyType,
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(), repaymentStartDateType, getSubmittedOnDate());
}
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;
}
public LocalDate fetchInterestRecalculateFromDate() {
LocalDate recalculatedOn;
if (this.interestRecalculatedOn == null) {
@@ -2602,7 +2223,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
Money transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency());
if (loanTransaction.isOverPaid()) {
// in case of advanced payment strategy and creditAllocations the full amount is recognized first
if (this.getCreditAllocationRules() != null && this.getCreditAllocationRules().size() > 0) {
if (this.getCreditAllocationRules() != null && !this.getCreditAllocationRules().isEmpty()) {
Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() //
.map(mapping -> mapping.getPrincipalPortion(getCurrency())) //
.reduce(Money.zero(getCurrency()), Money::plus);
@@ -2690,137 +2311,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return getRepaymentScheduleInstallments().stream().filter(predicate).toList();
}
/**
* @return loan disbursement data
**/
public List<DisbursementData> getDisbursementData() {
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 = loanDisbursementDetails.expectedDisbursementDate();
}
if (loanDisbursementDetails.actualDisbursementDate() != null) {
actualDisbursementDate = loanDisbursementDetails.actualDisbursementDate();
}
BigDecimal waivedChargeAmount = null;
disbursementData.add(new DisbursementData(loanDisbursementDetails.getId(), expectedDisbursementDate, actualDisbursementDate,
loanDisbursementDetails.principal(), this.netDisbursalAmount, null, null, waivedChargeAmount));
}
return disbursementData;
}
/**
* @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();
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.getGraceOnPrincipalPayment();
final Integer graceOnInterestPayment = this.loanRepaymentScheduleDetail.getGraceOnInterestPayment();
final Integer graceOnInterestCharged = this.loanRepaymentScheduleDetail.getGraceOnInterestCharged();
final LocalDate interestChargedFromDate = getInterestChargedFromDate();
final Integer graceOnArrearsAgeing = this.loanRepaymentScheduleDetail.getGraceOnArrearsAgeing();
final Money inArrearsToleranceMoney = this.loanRepaymentScheduleDetail.getInArrearsTolerance();
final BigDecimal emiAmount = getFixedEmiAmount();
final BigDecimal maxOutstandingBalance = getMaxOutstandingLoanBalance();
final List<DisbursementData> disbursementData = getDisbursementData();
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;
RepaymentStartDateType repaymentStartDateType = loanProduct.getRepaymentStartDateType();
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.toData(), 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, repaymentStartDateType, getSubmittedOnDate());
}
public void updateRescheduledByUser(AppUser rescheduledByUser) {
this.rescheduledByUser = rescheduledByUser;
}
@@ -2993,7 +2483,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public LoanRepaymentScheduleInstallment fetchLoanForeclosureDetail(final LocalDate closureDate) {
Money[] receivables = retriveIncomeOutstandingTillDate(closureDate);
Money[] receivables = retrieveIncomeOutstandingTillDate(closureDate);
Money totalPrincipal = Money.of(getCurrency(), this.getSummary().getTotalPrincipalOutstanding());
totalPrincipal = totalPrincipal.minus(receivables[3]);
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null;
@@ -3002,7 +2492,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
receivables[0].getAmount(), receivables[1].getAmount(), receivables[2].getAmount(), false, compoundingDetails);
}
public Money[] retriveIncomeOutstandingTillDate(final LocalDate paymentDate) {
private Money[] retrieveIncomeOutstandingTillDate(final LocalDate paymentDate) {
Money[] balances = new Money[4];
final MonetaryCurrency currency = getCurrency();
Money interest = Money.zero(currency);
@@ -3058,13 +2548,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
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 = DateUtils.getExactDifferenceInDays(installment.getFromDate(), installment.getDueDate());
int tillDays = DateUtils.getExactDifferenceInDays(installment.getFromDate(), paymentDate);
interestForCurrentPeriod = Money.of(getCurrency(), BigDecimal
Money interestForCurrentPeriod = Money.of(getCurrency(), BigDecimal
.valueOf(calculateInterestForDays(totalPeriodDays, installment.getInterestCharged(getCurrency()).getAmount(), tillDays)));
interestAccountedForCurrentPeriod = installment.getInterestWaived(getCurrency()).plus(installment.getInterestPaid(getCurrency()));
Money interestAccountedForCurrentPeriod = installment.getInterestWaived(getCurrency())
.plus(installment.getInterestPaid(getCurrency()));
for (LoanCharge loanCharge : this.charges) {
if (loanCharge.isActive() && !loanCharge.isDueAtDisbursement()) {
boolean isDue = loanCharge.isDueInPeriod(installment.getFromDate(), paymentDate, isFirstNormalInstallment);
@@ -3101,7 +2590,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return balances;
}
public Money[] retriveIncomeForOverlappingPeriod(final LocalDate paymentDate) {
public Money[] retrieveIncomeForOverlappingPeriod(final LocalDate paymentDate) {
Money[] balances = new Money[3];
final MonetaryCurrency currency = getCurrency();
balances[0] = balances[1] = balances[2] = Money.zero(currency);
@@ -3165,19 +2654,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.fraud = value;
}
public BigDecimal getFirstDisbursalAmount() {
BigDecimal firstDisbursalAmount;
if (this.isMultiDisburmentLoan()) {
List<DisbursementData> disbursementData = getDisbursementData();
Collections.sort(disbursementData);
firstDisbursalAmount = disbursementData.get(disbursementData.size() - 1).getPrincipal();
} else {
firstDisbursalAmount = this.getLoanRepaymentScheduleDetail().getPrincipal().getAmount();
}
return firstDisbursalAmount;
}
public void setTopupLoanDetails(LoanTopupDetails topupLoanDetails) {
this.loanTopupDetails = topupLoanDetails;
}
@@ -0,0 +1,251 @@
/**
* 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.mapper;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeLoanTransactionDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class LoanAccountingBridgeMapper {
public List<AccountingBridgeDataDTO> deriveAccountingBridgeDataForChargeOff(final String currencyCode,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final boolean isAccountTransfer,
final Loan loan) {
final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsBeforeChargeOff = new ArrayList<>();
final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsAfterChargeOff = new ArrayList<>();
// split the transactions according charge-off date
classifyTransactionsBasedOnChargeOffDate(newLoanTransactionsBeforeChargeOff, newLoanTransactionsAfterChargeOff,
existingTransactionIds, existingReversedTransactionIds, currencyCode, loan);
AccountingBridgeDataDTO beforeChargeOff = new AccountingBridgeDataDTO(loan.getId(), loan.productId(), loan.getOfficeId(),
currencyCode, loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
isAccountTransfer, false, loan.isFraud(), loan.fetchChargeOffReasonId(), newLoanTransactionsBeforeChargeOff);
AccountingBridgeDataDTO afterChargeOff = new AccountingBridgeDataDTO(loan.getId(), loan.productId(), loan.getOfficeId(),
currencyCode, loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
isAccountTransfer, true, loan.isFraud(), loan.fetchChargeOffReasonId(), newLoanTransactionsAfterChargeOff);
List<AccountingBridgeDataDTO> result = new ArrayList<>();
result.add(beforeChargeOff);
result.add(afterChargeOff);
return result;
}
public AccountingBridgeDataDTO deriveAccountingBridgeData(final String currencyCode, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final boolean isAccountTransfer, final Loan loan) {
final List<AccountingBridgeLoanTransactionDTO> newLoanTransactions = new ArrayList<>();
for (final LoanTransaction transaction : loan.getLoanTransactions()) {
if (transaction.isReversed() && existingTransactionIds.contains(transaction.getId())
&& !existingReversedTransactionIds.contains(transaction.getId())) {
newLoanTransactions.add(mapToLoanTransactionData(transaction, currencyCode));
} else if (!existingTransactionIds.contains(transaction.getId())) {
newLoanTransactions.add(mapToLoanTransactionData(transaction, currencyCode));
}
}
return new AccountingBridgeDataDTO(loan.getId(), loan.productId(), loan.getOfficeId(), currencyCode,
loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
isAccountTransfer, loan.isChargedOff(), loan.isFraud(), loan.fetchChargeOffReasonId(), newLoanTransactions);
}
public AccountingBridgeLoanTransactionDTO mapToLoanTransactionData(final LoanTransaction transaction, final String currencyCode) {
final AccountingBridgeLoanTransactionDTO transactionDTO = new AccountingBridgeLoanTransactionDTO();
transactionDTO.setId(transaction.getId());
transactionDTO.setOfficeId(transaction.getOffice().getId());
transactionDTO.setType(LoanEnumerations.transactionType(transaction.getTypeOf()));
transactionDTO.setReversed(transaction.isReversed());
transactionDTO.setDate(transaction.getTransactionDate());
transactionDTO.setCurrencyCode(currencyCode);
transactionDTO.setAmount(transaction.getAmount());
transactionDTO.setNetDisbursalAmount(transaction.getLoan().getNetDisbursalAmount());
if (transactionDTO.getType().isChargeback() && (transaction.getLoan().getCreditAllocationRules() == null
|| transaction.getLoan().getCreditAllocationRules().isEmpty())) {
transactionDTO.setPrincipalPortion(transaction.getAmount());
} else {
transactionDTO.setPrincipalPortion(transaction.getPrincipalPortion());
}
transactionDTO.setInterestPortion(transaction.getInterestPortion());
transactionDTO.setFeeChargesPortion(transaction.getFeeChargesPortion());
transactionDTO.setPenaltyChargesPortion(transaction.getPenaltyChargesPortion());
transactionDTO.setOverPaymentPortion(transaction.getOverPaymentPortion());
if (transactionDTO.getType().isChargeRefund()) {
transactionDTO.setChargeRefundChargeType(transaction.getChargeRefundChargeType());
}
if (transaction.getPaymentDetail() != null) {
transactionDTO.setPaymentTypeId(transaction.getPaymentDetail().getPaymentType().getId());
}
if (!transaction.getLoanChargesPaid().isEmpty()) {
List<LoanChargePaidByDTO> loanChargesPaidData = new ArrayList<>();
for (final LoanChargePaidBy chargePaidBy : transaction.getLoanChargesPaid()) {
final LoanChargePaidByDTO loanChargePaidData = new LoanChargePaidByDTO();
loanChargePaidData.setChargeId(chargePaidBy.getLoanCharge().getCharge().getId());
loanChargePaidData.setIsPenalty(chargePaidBy.getLoanCharge().isPenaltyCharge());
loanChargePaidData.setLoanChargeId(chargePaidBy.getLoanCharge().getId());
loanChargePaidData.setAmount(chargePaidBy.getAmount());
loanChargePaidData.setInstallmentNumber(chargePaidBy.getInstallmentNumber());
loanChargesPaidData.add(loanChargePaidData);
}
transactionDTO.setLoanChargesPaid(loanChargesPaidData);
}
if (transactionDTO.getType().isChargeback() && transaction.getOverPaymentPortion() != null
&& transaction.getOverPaymentPortion().compareTo(BigDecimal.ZERO) > 0) {
BigDecimal principalPaid = transaction.getOverPaymentPortion();
BigDecimal feePaid = BigDecimal.ZERO;
BigDecimal penaltyPaid = BigDecimal.ZERO;
if (!transaction.getLoanTransactionToRepaymentScheduleMappings().isEmpty()) {
principalPaid = transaction.getLoanTransactionToRepaymentScheduleMappings().stream()
.map(mapping -> Optional.ofNullable(mapping.getPrincipalPortion()).orElse(BigDecimal.ZERO))
.reduce(BigDecimal.ZERO, BigDecimal::add);
feePaid = transaction.getLoanTransactionToRepaymentScheduleMappings().stream()
.map(mapping -> Optional.ofNullable(mapping.getFeeChargesPortion()).orElse(BigDecimal.ZERO))
.reduce(BigDecimal.ZERO, BigDecimal::add);
penaltyPaid = transaction.getLoanTransactionToRepaymentScheduleMappings().stream()
.map(mapping -> Optional.ofNullable(mapping.getPenaltyChargesPortion()).orElse(BigDecimal.ZERO))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
transactionDTO.setPrincipalPaid(principalPaid);
transactionDTO.setFeePaid(feePaid);
transactionDTO.setPenaltyPaid(penaltyPaid);
}
LoanTransactionRelation loanTransactionRelation = transaction.getLoanTransactionRelations().stream()
.filter(e -> LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT.equals(e.getRelationType())).findAny().orElse(null);
if (loanTransactionRelation != null) {
LoanCharge loanCharge = loanTransactionRelation.getToCharge();
transactionDTO.setLoanChargeData(loanCharge.toData());
}
transactionDTO.setLoanToLoanTransfer(false);
return transactionDTO;
}
private void classifyTransactionsBasedOnChargeOffDate(final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsBeforeChargeOff,
final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsAfterChargeOff, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final String currencyCode, final Loan loan) {
// Before
filterTransactionsByChargeOffDate(newLoanTransactionsBeforeChargeOff, currencyCode, existingTransactionIds,
existingReversedTransactionIds,
transaction -> DateUtils.isBefore(transaction.getTransactionDate(), loan.getChargedOffOnDate()), loan);
// On
filterTransactionsByChargeOffDate(newLoanTransactionsBeforeChargeOff, newLoanTransactionsAfterChargeOff, currencyCode,
existingTransactionIds, existingReversedTransactionIds,
transaction -> DateUtils.isEqual(transaction.getTransactionDate(), loan.getChargedOffOnDate()), loan);
// After
filterTransactionsByChargeOffDate(newLoanTransactionsAfterChargeOff, currencyCode, existingTransactionIds,
existingReversedTransactionIds,
transaction -> DateUtils.isAfter(transaction.getTransactionDate(), loan.getChargedOffOnDate()), loan);
}
private void filterTransactionsByChargeOffDate(final List<AccountingBridgeLoanTransactionDTO> filteredTransactions,
final String currencyCode, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final Predicate<LoanTransaction> chargeOffDateCriteria, final Loan loan) {
filteredTransactions.addAll(loan.getLoanTransactions().stream() //
.filter(chargeOffDateCriteria) //
.filter(transaction -> {
boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId());
boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId());
if (transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction) {
return true;
} else {
return !isExistingTransaction;
}
}) //
.map(transaction -> mapToLoanTransactionData(transaction, currencyCode)).toList());
}
private void filterTransactionsByChargeOffDate(final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsBeforeChargeOff,
final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsAfterChargeOff, final String currencyCode,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final Predicate<LoanTransaction> chargeOffDateCriteria, final Loan loan) {
final Optional<LoanTransaction> chargeOffTransactionOptional = loan.getLoanTransactions().stream() //
.filter(LoanTransaction::isChargeOff) //
.filter(LoanTransaction::isNotReversed) //
.findFirst();
if (chargeOffTransactionOptional.isEmpty()) {
return;
}
final LoanTransaction chargeOffTransaction = chargeOffTransactionOptional.get();
final LoanTransaction originalChargeOffTransaction = getOriginalTransactionIfReverseReplayed(chargeOffTransaction);
loan.getLoanTransactions().stream().filter(chargeOffDateCriteria).forEach(transaction -> {
boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId());
boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId());
List<AccountingBridgeLoanTransactionDTO> targetList = null;
if ((transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction)) {
// reversed transactions
LoanTransaction originalTransaction = getOriginalTransactionIfReverseReplayed(transaction);
targetList = originalTransaction.happenedBefore(originalChargeOffTransaction) ? newLoanTransactionsBeforeChargeOff
: newLoanTransactionsAfterChargeOff;
} else if (!isExistingTransaction) {
// new and replayed transactions
targetList = transaction.happenedBefore(chargeOffTransaction) ? newLoanTransactionsBeforeChargeOff
: newLoanTransactionsAfterChargeOff;
}
if (targetList != null) {
targetList.add(mapToLoanTransactionData(transaction, currencyCode));
}
});
}
private LoanTransaction getOriginalTransactionIfReverseReplayed(final LoanTransaction loanTransaction) {
if (!loanTransaction.getLoanTransactionRelations().isEmpty()) {
return loanTransaction.getLoanTransactionRelations().stream()
.filter(tr -> LoanTransactionRelationTypeEnum.REPLAYED.equals(tr.getRelationType()))
.map(LoanTransactionRelation::getToTransaction).toList().stream().min(Comparator.comparingLong(LoanTransaction::getId))
.orElse(loanTransaction);
}
return loanTransaction;
}
}
@@ -0,0 +1,102 @@
/**
* 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.mapper;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.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.loanproduct.domain.InterestMethod;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class LoanMapper {
private final LoanTermVariationsMapper loanTermVariationsMapper;
public LoanScheduleModel regenerateScheduleModel(final ScheduleGeneratorDTO scheduleGeneratorDTO, final Loan loan) {
final MathContext mc = MoneyHelper.getMathContext();
final LoanApplicationTerms loanApplicationTerms = loanTermVariationsMapper.constructLoanApplicationTerms(scheduleGeneratorDTO,
loan);
LoanScheduleGenerator loanScheduleGenerator;
if (loanApplicationTerms.isEqualAmortization()) {
if (loanApplicationTerms.getInterestMethod().isDecliningBalance()) {
final LoanScheduleGenerator decliningLoanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory()
.create(loanApplicationTerms.getLoanScheduleType(), InterestMethod.DECLINING_BALANCE);
Set<LoanCharge> loanCharges = loan.getActiveCharges();
LoanScheduleModel loanSchedule = decliningLoanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges,
scheduleGeneratorDTO.getHolidayDetailDTO());
loanApplicationTerms
.updateTotalInterestDue(Money.of(loanApplicationTerms.getCurrency(), loanSchedule.getTotalInterestCharged()));
}
loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(loanApplicationTerms.getLoanScheduleType(),
InterestMethod.FLAT);
} else {
loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(loanApplicationTerms.getLoanScheduleType(),
loanApplicationTerms.getInterestMethod());
}
return loanScheduleGenerator.generate(mc, loanApplicationTerms, loan.getActiveCharges(),
scheduleGeneratorDTO.getHolidayDetailDTO());
}
public List<DisbursementData> getDisbursementData(final Loan loan) {
Iterator<LoanDisbursementDetails> iterator = loan.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 = loanDisbursementDetails.expectedDisbursementDate();
}
if (loanDisbursementDetails.actualDisbursementDate() != null) {
actualDisbursementDate = loanDisbursementDetails.actualDisbursementDate();
}
BigDecimal waivedChargeAmount = null;
disbursementData.add(new DisbursementData(loanDisbursementDetails.getId(), expectedDisbursementDate, actualDisbursementDate,
loanDisbursementDetails.principal(), loan.getNetDisbursalAmount(), null, null, waivedChargeAmount));
}
return disbursementData;
}
}
@@ -0,0 +1,148 @@
/**
* 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.mapper;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.calendar.data.CalendarHistoryDataWrapper;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.common.domain.DayOfWeekType;
import org.apache.fineract.portfolio.common.domain.NthDayType;
import org.apache.fineract.portfolio.floatingrates.data.FloatingRateDTO;
import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.springframework.stereotype.Component;
@Component
public class LoanTermVariationsMapper {
public BigDecimal constructLoanTermVariations(final FloatingRateDTO floatingRateDTO, final BigDecimal annualNominalInterestRate,
final List<LoanTermVariationsData> loanTermVariations, final Loan loan) {
for (LoanTermVariations variationTerms : loan.getLoanTermVariations()) {
if (variationTerms.isActive()) {
loanTermVariations.add(variationTerms.toData());
}
}
return constructFloatingInterestRates(annualNominalInterestRate, floatingRateDTO, loanTermVariations, loan);
}
public LoanApplicationTerms constructLoanApplicationTerms(final ScheduleGeneratorDTO scheduleGeneratorDTO, final Loan loan) {
final Integer loanTermFrequency = loan.getTermFrequency();
NthDayType nthDayType = null;
DayOfWeekType dayOfWeekType = null;
final List<DisbursementData> disbursementData = new ArrayList<>();
for (LoanDisbursementDetails disbursementDetails : loan.getDisbursementDetails()) {
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;
RepaymentStartDateType repaymentStartDateType = loan.getLoanProduct().getRepaymentStartDateType();
boolean allowCompoundingOnEod = false;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
restCalendarInstance = scheduleGeneratorDTO.getCalendarInstanceForInterestRecalculation();
compoundingCalendarInstance = scheduleGeneratorDTO.getCompoundingCalendarInstance();
recalculationFrequencyType = loan.getLoanInterestRecalculationDetails().getRestFrequencyType();
compoundingMethod = loan.getLoanInterestRecalculationDetails().getInterestRecalculationCompoundingMethod();
compoundingFrequencyType = loan.getLoanInterestRecalculationDetails().getCompoundingFrequencyType();
rescheduleStrategyMethod = loan.getLoanInterestRecalculationDetails().getRescheduleStrategyMethod();
allowCompoundingOnEod = loan.getLoanInterestRecalculationDetails().allowCompoundingOnEod();
}
calendar = scheduleGeneratorDTO.getCalendar();
calendarHistoryDataWrapper = scheduleGeneratorDTO.getCalendarHistoryDataWrapper();
BigDecimal annualNominalInterestRate = loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate();
FloatingRateDTO floatingRateDTO = scheduleGeneratorDTO.getFloatingRateDTO();
List<LoanTermVariationsData> loanTermVariations = new ArrayList<>();
annualNominalInterestRate = constructLoanTermVariations(floatingRateDTO, annualNominalInterestRate, loanTermVariations, loan);
LocalDate interestChargedFromDate = loan.getInterestChargedFromDate();
if (interestChargedFromDate == null && scheduleGeneratorDTO.isInterestChargedFromDateAsDisbursementDateEnabled()) {
interestChargedFromDate = loan.getDisbursementDate();
}
return LoanApplicationTerms.assembleFrom(scheduleGeneratorDTO.getCurrency(), loanTermFrequency, loan.getTermPeriodFrequencyType(),
nthDayType, dayOfWeekType, loan.getDisbursementDate(), loan.getExpectedFirstRepaymentOnDate(),
scheduleGeneratorDTO.getCalculatedRepaymentsStartingFromDate(), loan.getInArrearsTolerance(),
loan.getLoanRepaymentScheduleDetail(), loan.getLoanProduct().isMultiDisburseLoan(), loan.getFixedEmiAmount(),
disbursementData, loan.getMaxOutstandingLoanBalance(), interestChargedFromDate,
loan.getLoanProduct().getPrincipalThresholdForLastInstallment(), loan.getLoanProduct().getInstallmentAmountInMultiplesOf(),
recalculationFrequencyType, restCalendarInstance, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType,
loan.getLoanProduct().preCloseInterestCalculationStrategy(), rescheduleStrategyMethod, calendar,
loan.getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, calendarHistoryDataWrapper,
scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), holidayDetailDTO,
allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday(),
scheduleGeneratorDTO.isInterestToBeRecoveredFirstWhenGreaterThanEMI(), loan.getFixedPrincipalPercentagePerInstallment(),
scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans(), repaymentStartDateType, loan.getSubmittedOnDate());
}
private BigDecimal constructFloatingInterestRates(final BigDecimal annualNominalInterestRate, final FloatingRateDTO floatingRateDTO,
final List<LoanTermVariationsData> loanTermVariations, final Loan loan) {
final LocalDate dateValue = null;
final boolean isSpecificToInstallment = false;
BigDecimal interestRate = annualNominalInterestRate;
if (loan.getLoanProduct().isLinkedToFloatingInterestRate()) {
floatingRateDTO.resetInterestRateDiff();
Collection<FloatingRatePeriodData> applicableRates = loan.getLoanProduct().fetchInterestRates(floatingRateDTO);
LocalDate interestRateStartDate = DateUtils.getBusinessLocalDate();
for (FloatingRatePeriodData periodData : applicableRates) {
LoanTermVariationsData loanTermVariation = new LoanTermVariationsData(
LoanEnumerations.loanVariationType(LoanTermVariationType.INTEREST_RATE), periodData.getFromDateAsLocalDate(),
periodData.getInterestRate(), dateValue, isSpecificToInstallment);
if (!DateUtils.isBefore(interestRateStartDate, periodData.getFromDateAsLocalDate())) {
interestRateStartDate = periodData.getFromDateAsLocalDate();
interestRate = periodData.getInterestRate();
}
loanTermVariations.add(loanTermVariation);
}
}
return interestRate;
}
}
@@ -29,6 +29,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanMapper;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
@RequiredArgsConstructor
@@ -36,6 +37,7 @@ public class LoanScheduleService {
private final LoanChargeService loanChargeService;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanMapper loanMapper;
private final LoanTransactionProcessingService loadTransactionProcessingService;
private final LoanScheduleComponent loanSchedule;
@@ -43,7 +45,7 @@ public class LoanScheduleService {
* Ability to regenerate the repayment schedule based on the loans current details/state.
*/
public void regenerateRepaymentSchedule(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
final LoanScheduleModel loanScheduleModel = loan.regenerateScheduleModel(scheduleGeneratorDTO);
final LoanScheduleModel loanScheduleModel = loanMapper.regenerateScheduleModel(scheduleGeneratorDTO, loan);
if (loanScheduleModel == null) {
return;
}
@@ -41,6 +41,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Tra
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanTermVariationsMapper;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.springframework.stereotype.Service;
@@ -49,6 +50,7 @@ import org.springframework.stereotype.Service;
public class LoanTransactionProcessingService {
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final LoanTermVariationsMapper loanMapper;
public ChangedTransactionDetail processLatestTransaction(String transactionProcessingStrategyCode, LoanTransaction loanTransaction,
TransactionCtx ctx) {
@@ -103,7 +105,7 @@ public class LoanTransactionProcessingService {
final MathContext mc = MoneyHelper.getMathContext();
final LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(generatorDTO);
final LoanApplicationTerms loanApplicationTerms = loanMapper.constructLoanApplicationTerms(generatorDTO, loan);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(
loan.getTransactionProcessingStrategyCode());
@@ -119,7 +121,7 @@ public class LoanTransactionProcessingService {
final MathContext mc = MoneyHelper.getMathContext();
final InterestMethod interestMethod = loan.getLoanRepaymentScheduleDetail().getInterestMethod();
final LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(scheduleGeneratorDTO);
final LoanApplicationTerms loanApplicationTerms = loanMapper.constructLoanApplicationTerms(scheduleGeneratorDTO, loan);
final LoanScheduleGenerator loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory()
.create(loanApplicationTerms.getLoanScheduleType(), interestMethod);