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);
@@ -29,8 +29,8 @@ import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.closure.domain.GLClosure;
import org.apache.fineract.accounting.closure.domain.GLClosureRepository;
import org.apache.fineract.accounting.common.AccountingConstants;
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForSavings;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares;
@@ -68,12 +68,11 @@ import org.apache.fineract.portfolio.PortfolioProductType;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.ClientTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeLoanTransactionDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionEnumData;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository;
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountTransactionEnumData;
import org.springframework.dao.DataAccessException;
@@ -92,55 +91,53 @@ public class AccountingProcessorHelper {
private final GLClosureRepository closureRepository;
private final GLAccountRepository glAccountRepository;
private final OfficeRepository officeRepository;
private final LoanTransactionRepository loanTransactionRepository;
private final ClientTransactionRepository clientTransactionRepository;
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
private final AccountTransfersReadPlatformService accountTransfersReadPlatformService;
private final ChargeRepositoryWrapper chargeRepositoryWrapper;
private final BusinessEventNotifierService businessEventNotifierService;
public LoanDTO populateLoanDtoFromMap(final Map<String, Object> accountingBridgeData, final boolean cashBasedAccountingEnabled,
final boolean upfrontAccrualBasedAccountingEnabled, final boolean periodicAccrualBasedAccountingEnabled) {
final Long loanId = (Long) accountingBridgeData.get("loanId");
final Long loanProductId = (Long) accountingBridgeData.get("loanProductId");
final Long officeId = (Long) accountingBridgeData.get("officeId");
final String currencyCode = (String) accountingBridgeData.get("currencyCode");
public LoanDTO populateLoanDtoFromDTO(
final org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO accountingBridgeData) {
final Long loanId = accountingBridgeData.getLoanId();
final Long loanProductId = accountingBridgeData.getLoanProductId();
final Long officeId = accountingBridgeData.getOfficeId();
final String currencyCode = accountingBridgeData.getCurrencyCode();
final List<LoanTransactionDTO> newLoanTransactions = new ArrayList<>();
boolean isAccountTransfer = (Boolean) accountingBridgeData.get("isAccountTransfer");
boolean isLoanMarkedAsChargeOff = (Boolean) accountingBridgeData.get("isChargeOff");
boolean isLoanMarkedAsFraud = (Boolean) accountingBridgeData.get("isFraud");
final Long chargeOffReasonCodeValue = (Long) accountingBridgeData.get("chargeOffReasonCodeValue");
boolean isAccountTransfer = accountingBridgeData.isAccountTransfer();
boolean isLoanMarkedAsChargeOff = accountingBridgeData.isChargeOff();
boolean isLoanMarkedAsFraud = accountingBridgeData.isFraud();
final Long chargeOffReasonCodeValue = accountingBridgeData.getChargeOffReasonCodeValue();
final boolean cashBasedAccountingEnabled = accountingBridgeData.isCashBasedAccountingEnabled();
final boolean upfrontAccrualBasedAccountingEnabled = accountingBridgeData.isUpfrontAccrualBasedAccountingEnabled();
final boolean periodicAccrualBasedAccountingEnabled = accountingBridgeData.isPeriodicAccrualBasedAccountingEnabled();
@SuppressWarnings("unchecked")
final List<Map<String, Object>> newTransactionsMap = (List<Map<String, Object>>) accountingBridgeData.get("newLoanTransactions");
final List<AccountingBridgeLoanTransactionDTO> loanTransactionDTOs = accountingBridgeData.getNewLoanTransactions();
for (final Map<String, Object> map : newTransactionsMap) {
final Long transactionOfficeId = (Long) map.get("officeId");
final String transactionId = ((Long) map.get("id")).toString();
final LocalDate transactionDate = ((LocalDate) map.get("date"));
final LoanTransactionEnumData transactionType = (LoanTransactionEnumData) map.get("type");
final BigDecimal amount = (BigDecimal) map.get("amount");
final BigDecimal principal = (BigDecimal) map.get("principalPortion");
final BigDecimal interest = (BigDecimal) map.get("interestPortion");
final BigDecimal fees = (BigDecimal) map.get("feeChargesPortion");
final BigDecimal penalties = (BigDecimal) map.get("penaltyChargesPortion");
final BigDecimal overPayments = (BigDecimal) map.get("overPaymentPortion");
final boolean reversed = (Boolean) map.get("reversed");
final Long paymentTypeId = (Long) map.get("paymentTypeId");
final String chargeRefundChargeType = (String) map.get("chargeRefundChargeType");
final LoanChargeData loanChargeData = (LoanChargeData) map.get("loanChargeData");
for (final AccountingBridgeLoanTransactionDTO loanTxnDto : loanTransactionDTOs) {
final Long transactionOfficeId = loanTxnDto.getOfficeId();
final String transactionId = loanTxnDto.getId().toString();
final LocalDate transactionDate = loanTxnDto.getDate();
final LoanTransactionEnumData transactionType = loanTxnDto.getType();
final BigDecimal amount = loanTxnDto.getAmount();
final BigDecimal principal = loanTxnDto.getPrincipalPortion();
final BigDecimal interest = loanTxnDto.getInterestPortion();
final BigDecimal fees = loanTxnDto.getFeeChargesPortion();
final BigDecimal penalties = loanTxnDto.getPenaltyChargesPortion();
final BigDecimal overPayments = loanTxnDto.getOverPaymentPortion();
final boolean reversed = loanTxnDto.isReversed();
final Long paymentTypeId = loanTxnDto.getPaymentTypeId();
final String chargeRefundChargeType = loanTxnDto.getChargeRefundChargeType();
final LoanChargeData loanChargeData = loanTxnDto.getLoanChargeData();
final List<ChargePaymentDTO> feePaymentDetails = new ArrayList<>();
final List<ChargePaymentDTO> penaltyPaymentDetails = new ArrayList<>();
// extract charge payment details (if exists)
if (map.containsKey("loanChargesPaid")) {
@SuppressWarnings("unchecked")
final List<Map<String, Object>> loanChargesPaidData = (List<Map<String, Object>>) map.get("loanChargesPaid");
for (final Map<String, Object> loanChargePaid : loanChargesPaidData) {
final Long chargeId = (Long) loanChargePaid.get("chargeId");
final Long loanChargeId = (Long) loanChargePaid.get("loanChargeId");
final boolean isPenalty = (Boolean) loanChargePaid.get("isPenalty");
final BigDecimal chargeAmountPaid = (BigDecimal) loanChargePaid.get("amount");
if (loanTxnDto.getLoanChargesPaid() != null) {
List<LoanChargePaidByDTO> loanChargesPaidData = loanTxnDto.getLoanChargesPaid();
for (final LoanChargePaidByDTO loanChargePaid : loanChargesPaidData) {
final Long chargeId = loanChargePaid.getChargeId();
final Long loanChargeId = loanChargePaid.getLoanChargeId();
final boolean isPenalty = loanChargePaid.getIsPenalty();
final BigDecimal chargeAmountPaid = loanChargePaid.getAmount();
final ChargePaymentDTO chargePaymentDTO = new ChargePaymentDTO(chargeId, chargeAmountPaid, loanChargeId);
if (isPenalty) {
penaltyPaymentDetails.add(chargePaymentDTO);
@@ -150,23 +147,23 @@ public class AccountingProcessorHelper {
}
}
if (!isAccountTransfer) {
isAccountTransfer = this.accountTransfersReadPlatformService.isAccountTransfer(Long.parseLong(transactionId),
boolean localIsAccountTransfer = isAccountTransfer;
if (!localIsAccountTransfer) {
localIsAccountTransfer = this.accountTransfersReadPlatformService.isAccountTransfer(Long.parseLong(transactionId),
PortfolioAccountType.LOAN);
}
BigDecimal principalPaid = (BigDecimal) map.get("principalPaid");
BigDecimal feePaid = (BigDecimal) map.get("feePaid");
BigDecimal penaltyPaid = (BigDecimal) map.get("penaltyPaid");
BigDecimal principalPaid = loanTxnDto.getPrincipalPaid();
BigDecimal feePaid = loanTxnDto.getFeePaid();
BigDecimal penaltyPaid = loanTxnDto.getPenaltyPaid();
final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId,
transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, reversed,
penaltyPaymentDetails, feePaymentDetails, isAccountTransfer, chargeRefundChargeType, loanChargeData, principalPaid,
penaltyPaymentDetails, feePaymentDetails, localIsAccountTransfer, chargeRefundChargeType, loanChargeData, principalPaid,
feePaid, penaltyPaid);
Boolean isLoanToLoanTransfer = (Boolean) accountingBridgeData.get("isLoanToLoanTransfer");
transaction.setLoanToLoanTransfer(isLoanToLoanTransfer != null && isLoanToLoanTransfer);
newLoanTransactions.add(transaction);
transaction.setLoanToLoanTransfer(loanTxnDto.isLoanToLoanTransfer());
newLoanTransactions.add(transaction);
}
return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled,
@@ -559,10 +556,10 @@ public class AccountingProcessorHelper {
}
public void createAccrualBasedJournalEntriesAndReversalsForSavingsTax(final Office office, final String currencyCode,
final AccrualAccountsForSavings accountTypeToBeDebited, final AccrualAccountsForSavings accountTypeToBeCredited,
final Long savingsProductId, final Long paymentTypeId, final Long savingsId, final String transactionId,
final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal, final List<TaxPaymentDTO> taxDetails) {
final AccountingConstants.AccrualAccountsForSavings accountTypeToBeDebited,
final AccountingConstants.AccrualAccountsForSavings accountTypeToBeCredited, final Long savingsProductId,
final Long paymentTypeId, final Long savingsId, final String transactionId, final LocalDate transactionDate,
final BigDecimal amount, final Boolean isReversal, final List<TaxPaymentDTO> taxDetails) {
for (TaxPaymentDTO taxPaymentDTO : taxDetails) {
if (taxPaymentDTO.getAmount() != null) {
if (taxPaymentDTO.getCreditAccountId() == null) {
@@ -769,10 +766,10 @@ public class AccountingProcessorHelper {
}
public void createAccrualBasedJournalEntriesAndReversalsForSavingsCharges(final Office office, final String currencyCode,
final AccrualAccountsForSavings accountTypeToBeDebited, AccrualAccountsForSavings accountTypeToBeCredited,
final Long savingsProductId, final Long paymentTypeId, final Long loanId, final String transactionId,
final LocalDate transactionDate, final BigDecimal totalAmount, final Boolean isReversal,
final List<ChargePaymentDTO> chargePaymentDTOs) {
final AccountingConstants.AccrualAccountsForSavings accountTypeToBeDebited,
final AccountingConstants.AccrualAccountsForSavings accountTypeToBeCredited, final Long savingsProductId,
final Long paymentTypeId, final Long loanId, final String transactionId, final LocalDate transactionDate,
final BigDecimal totalAmount, final Boolean isReversal, final List<ChargePaymentDTO> chargePaymentDTOs) {
// TODO Vishwas: Remove this validation, as and when appropriate Junit
// tests are written for accounting
/**
@@ -24,6 +24,7 @@ import java.util.Map;
import org.apache.fineract.accounting.provisioning.domain.ProvisioningEntry;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
public interface JournalEntryWritePlatformService {
@@ -31,7 +32,7 @@ public interface JournalEntryWritePlatformService {
CommandProcessingResult revertJournalEntry(JsonCommand command);
void createJournalEntriesForLoan(Map<String, Object> accountingBridgeData);
void createJournalEntriesForLoan(AccountingBridgeDataDTO accountingBridgeData);
void createJournalEntriesForSavings(Map<String, Object> accountingBridgeData);
@@ -75,6 +75,7 @@ import org.apache.fineract.organisation.monetary.domain.OrganisationCurrencyRepo
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.PortfolioProductType;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.useradministration.domain.AppUser;
@@ -480,15 +481,13 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
@Transactional
@Override
public void createJournalEntriesForLoan(final Map<String, Object> accountingBridgeData) {
final boolean cashBasedAccountingEnabled = (Boolean) accountingBridgeData.get("cashBasedAccountingEnabled");
final boolean upfrontAccrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("upfrontAccrualBasedAccountingEnabled");
final boolean periodicAccrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("periodicAccrualBasedAccountingEnabled");
public void createJournalEntriesForLoan(final AccountingBridgeDataDTO accountingBridgeData) {
final boolean cashBasedAccountingEnabled = accountingBridgeData.isCashBasedAccountingEnabled();
final boolean upfrontAccrualBasedAccountingEnabled = accountingBridgeData.isUpfrontAccrualBasedAccountingEnabled();
final boolean periodicAccrualBasedAccountingEnabled = accountingBridgeData.isPeriodicAccrualBasedAccountingEnabled();
if (cashBasedAccountingEnabled || upfrontAccrualBasedAccountingEnabled || periodicAccrualBasedAccountingEnabled) {
final LoanDTO loanDTO = this.helper.populateLoanDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled,
upfrontAccrualBasedAccountingEnabled, periodicAccrualBasedAccountingEnabled);
final LoanDTO loanDTO = this.helper.populateLoanDtoFromDTO(accountingBridgeData);
final AccountingProcessorForLoan accountingProcessorForLoan = this.accountingProcessorForLoanFactory
.determineProcessor(loanDTO);
accountingProcessorForLoan.createJournalEntriesForLoan(loanDTO);
@@ -46,10 +46,7 @@ import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.ClientTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransactionRepository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -63,14 +60,11 @@ public class AccountingJournalEntryConfiguration {
public AccountingProcessorHelper accountingProcessorHelper(JournalEntryRepository glJournalEntryRepository,
ProductToGLAccountMappingRepository accountMappingRepository,
FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository, GLClosureRepository closureRepository,
GLAccountRepository glAccountRepository, OfficeRepository officeRepository, LoanTransactionRepository loanTransactionRepository,
ClientTransactionRepository clientTransactionRepository,
SavingsAccountTransactionRepository savingsAccountTransactionRepository,
GLAccountRepository glAccountRepository, OfficeRepository officeRepository,
AccountTransfersReadPlatformService accountTransfersReadPlatformService, ChargeRepositoryWrapper chargeRepositoryWrapper,
BusinessEventNotifierService businessEventNotifierService) {
return new AccountingProcessorHelper(glJournalEntryRepository, accountMappingRepository, financialActivityAccountRepository,
closureRepository, glAccountRepository, officeRepository, loanTransactionRepository, clientTransactionRepository,
savingsAccountTransactionRepository, accountTransfersReadPlatformService, chargeRepositoryWrapper,
closureRepository, glAccountRepository, officeRepository, accountTransfersReadPlatformService, chargeRepositoryWrapper,
businessEventNotifierService);
}
@@ -87,6 +87,7 @@ import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatfor
import org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanRefundRequestData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
@@ -94,6 +95,7 @@ import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanForeclosureValidator;
@@ -157,6 +159,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final LoanRefundService loanRefundService;
private final LoanAccountService loanAccountService;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
@Transactional
@Override
@@ -449,19 +452,26 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final MonetaryCurrency currency = loanAccount.getCurrency();
List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
if (loanAccount.isChargedOff()) {
accountingBridgeData = loanAccount.deriveAccountingBridgeDataForChargeOff(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer);
} else {
accountingBridgeData.add(loanAccount.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer));
}
for (Map<String, Object> accountingData : accountingBridgeData) {
accountingData.put("isLoanToLoanTransfer", isLoanToLoanTransfer);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingData);
}
final List<AccountingBridgeDataDTO> accountingBridgeDataList = loanAccountingBridgeMapper
.deriveAccountingBridgeDataForChargeOff(currency.getCode(), existingTransactionIds, existingReversedTransactionIds,
isAccountTransfer, loanAccount);
if (isLoanToLoanTransfer) {
accountingBridgeDataList.forEach(dto -> dto.getNewLoanTransactions().forEach(tx -> tx.setLoanToLoanTransfer(true)));
}
for (AccountingBridgeDataDTO accountingBridgeData : accountingBridgeDataList) {
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
} else {
final AccountingBridgeDataDTO accountingBridgeData = loanAccountingBridgeMapper.deriveAccountingBridgeData(currency.getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, loanAccount);
if (isLoanToLoanTransfer) {
accountingBridgeData.getNewLoanTransactions().forEach(tx -> tx.setLoanToLoanTransfer(true));
}
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
}
private void checkClientOrGroupActive(final Loan loan) {
@@ -985,7 +995,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final List<LoanRepaymentScheduleInstallment> newInstallments = new ArrayList<>(loan.getRepaymentScheduleInstallments());
final MonetaryCurrency currency = loan.getCurrency();
Money totalPrincipal = Money.zero(currency);
final Money[] balances = loan.retriveIncomeForOverlappingPeriod(transactionDate);
final Money[] balances = loan.retrieveIncomeForOverlappingPeriod(transactionDate);
boolean isInterestComponent = true;
for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
if (!DateUtils.isAfter(transactionDate, installment.getDueDate())) {
@@ -42,6 +42,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanTermVariationsMapper;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
@@ -59,6 +60,7 @@ public class ApplyHolidaysToLoansTasklet implements Tasklet {
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanUtilService loanUtilService;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTermVariationsMapper loanTermVariationsMapper;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -120,7 +122,8 @@ public class ApplyHolidaysToLoansTasklet implements Tasklet {
private void adjustRepaymentSchedules(Loan loan, Holiday holiday, LocalDate adjustedRescheduleToDate) {
final DefaultScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, holiday.getFromDate());
final LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(scheduleGeneratorDTO);
final LoanApplicationTerms loanApplicationTerms = loanTermVariationsMapper.constructLoanApplicationTerms(scheduleGeneratorDTO,
loan);
// first repayment's from date is same as disbursement date.
LocalDate tmpFromDate = loan.getDisbursementDate();
@@ -152,7 +155,8 @@ public class ApplyHolidaysToLoansTasklet implements Tasklet {
private void adjustAllRepaymentSchedules(Loan loan, Holiday holiday, LocalDate adjustedRescheduleToDate) {
final DefaultScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, holiday.getFromDate());
final LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(scheduleGeneratorDTO);
final LoanApplicationTerms loanApplicationTerms = loanTermVariationsMapper.constructLoanApplicationTerms(scheduleGeneratorDTO,
loan);
// first repayment's from date is same as disbursement date.
LocalDate tmpFromDate = loan.getDisbursementDate();
@@ -44,6 +44,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleD
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanTermVariationsMapper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanScheduleValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
@@ -63,6 +64,7 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
private final LoanUtilService loanUtilService;
private final LoanRepositoryWrapper loanRepository;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
private final LoanTermVariationsMapper loanTermVariationsMapper;
@Override
public LoanScheduleModel calculateLoanSchedule(final JsonQuery query, Boolean validateParams) {
@@ -204,7 +206,7 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
private LoanApplicationTerms constructLoanApplicationTerms(final Loan loan) {
final LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
return loan.constructLoanApplicationTerms(scheduleGeneratorDTO);
return loanTermVariationsMapper.constructLoanApplicationTerms(scheduleGeneratorDTO, loan);
}
private Loan fetchLoan(final Long accountId) {
@@ -23,6 +23,7 @@ import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
@@ -38,32 +39,23 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplica
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanTermVariationsMapper;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.exception.LoanRescheduleRequestNotFoundException;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LoanReschedulePreviewPlatformServiceImpl implements LoanReschedulePreviewPlatformService {
private static final DefaultScheduledDateGenerator DEFAULT_SCHEDULED_DATE_GENERATOR = new DefaultScheduledDateGenerator();
private final LoanRescheduleRequestRepositoryWrapper loanRescheduleRequestRepository;
private final LoanUtilService loanUtilService;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final LoanScheduleGeneratorFactory loanScheduleFactory;
private static final DefaultScheduledDateGenerator DEFAULT_SCHEDULED_DATE_GENERATOR = new DefaultScheduledDateGenerator();
@Autowired
public LoanReschedulePreviewPlatformServiceImpl(final LoanRescheduleRequestRepositoryWrapper loanRescheduleRequestRepository,
final LoanUtilService loanUtilService,
final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
final LoanScheduleGeneratorFactory loanScheduleFactory) {
this.loanRescheduleRequestRepository = loanRescheduleRequestRepository;
this.loanUtilService = loanUtilService;
this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory;
this.loanScheduleFactory = loanScheduleFactory;
}
private final LoanTermVariationsMapper loanTermVariationsMapper;
@Override
public LoanScheduleModel previewLoanReschedule(Long requestId) {
@@ -80,7 +72,8 @@ public class LoanReschedulePreviewPlatformServiceImpl implements LoanRescheduleP
loanRescheduleRequest.getRescheduleFromDate());
LocalDate rescheduleFromDate = null;
List<LoanTermVariationsData> removeLoanTermVariationsData = new ArrayList<>();
final LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(scheduleGeneratorDTO);
final LoanApplicationTerms loanApplicationTerms = loanTermVariationsMapper.constructLoanApplicationTerms(scheduleGeneratorDTO,
loan);
LoanTermVariations dueDateVariationInCurrentRequest = loanRescheduleRequest.getDueDateTermVariationIfExists();
if (dueDateVariationInCurrentRequest != null) {
for (LoanTermVariationsData loanTermVariation : loanApplicationTerms.getLoanTermVariations().getDueDateVariation()) {
@@ -47,6 +47,7 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -68,6 +69,8 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanRepayme
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanTermVariationsMapper;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansApiConstants;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidator;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
@@ -116,6 +119,8 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanChargeService loanChargeService;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanTermVariationsMapper loanTermVariationsMapper;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
private final LoanScheduleComponent loanSchedule;
/**
@@ -363,7 +368,8 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
Collection<LoanRepaymentScheduleHistory> loanRepaymentScheduleHistoryList = this.loanScheduleHistoryWritePlatformService
.createLoanScheduleArchive(loan.getRepaymentScheduleInstallments(), loan, loanRescheduleRequest);
final LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(scheduleGeneratorDTO);
final LoanApplicationTerms loanApplicationTerms = loanTermVariationsMapper.constructLoanApplicationTerms(scheduleGeneratorDTO,
loan);
LocalDate rescheduleFromDate = null;
List<LoanTermVariations> activeLoanTermVariations = loan.getActiveLoanTermVariations();
@@ -403,7 +409,8 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
}
BigDecimal annualNominalInterestRate = null;
List<LoanTermVariationsData> loanTermVariations = new ArrayList<>();
loan.constructLoanTermVariations(scheduleGeneratorDTO.getFloatingRateDTO(), annualNominalInterestRate, loanTermVariations);
loanTermVariationsMapper.constructLoanTermVariations(scheduleGeneratorDTO.getFloatingRateDTO(), annualNominalInterestRate,
loanTermVariations, loan);
loanApplicationTerms.getLoanTermVariations().setExceptionData(loanTermVariations);
/*
@@ -495,8 +502,8 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
private void postJournalEntries(Loan loan, List<Long> existingTransactionIds, List<Long> existingReversedTransactionIds) {
final MonetaryCurrency currency = loan.getCurrency();
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer);
final AccountingBridgeDataDTO accountingBridgeData = loanAccountingBridgeMapper.deriveAccountingBridgeData(currency.getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, loan);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
@@ -28,6 +28,7 @@ import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -90,6 +91,7 @@ import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
@@ -109,6 +111,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementData
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanMapper;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
@@ -200,6 +203,7 @@ public final class LoanApplicationValidator {
private final CalendarInstanceRepository calendarInstanceRepository;
private final LoanUtilService loanUtilService;
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
private final LoanMapper loanMapper;
public void validateForCreate(final Loan loan) {
final LocalDate expectedFirstRepaymentOnDate = loan.getExpectedFirstRepaymentOnDate();
@@ -1404,12 +1408,11 @@ public final class LoanApplicationValidator {
BigDecimal loanOutstanding = this.loanReadPlatformService
.retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, expectedDisbursementDate)
.getAmount();
final BigDecimal firstDisbursalAmount = loan.getFirstDisbursalAmount();
final BigDecimal firstDisbursalAmount = getFirstDisbursalAmount(loan);
if (loanOutstanding.compareTo(firstDisbursalAmount) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed",
"Topup loan amount should be greater than outstanding amount of loan to be closed.");
}
}
}
@@ -1953,7 +1956,7 @@ public final class LoanApplicationValidator {
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
}
public void validateTopupLoan(Loan loan, LocalDate expectedDisbursementDate) {
public BigDecimal validateTopupLoan(final Loan loan, final LocalDate disbursementDate) {
final Long loanIdToClose = loan.getTopupLoanDetails().getLoanIdToClose();
final Loan loanToClose = loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, loan.getClientId());
if (loanToClose == null) {
@@ -1969,16 +1972,15 @@ public final class LoanApplicationValidator {
+ " should be after last transaction date of loan to be closed " + lastUserTransactionOnLoanToClose);
}
BigDecimal loanOutstanding = loanReadPlatformService
.retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, expectedDisbursementDate).getAmount();
final BigDecimal firstDisbursalAmount = loan.getFirstDisbursalAmount();
final BigDecimal loanOutstanding = loanReadPlatformService
.retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, disbursementDate).getAmount();
final BigDecimal firstDisbursalAmount = getFirstDisbursalAmount(loan);
if (loanOutstanding.compareTo(firstDisbursalAmount) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed",
"Topup loan amount should be greater than outstanding amount of loan to be closed.");
}
BigDecimal netDisbursalAmount = loan.getApprovedPrincipal().subtract(loanOutstanding);
loan.adjustNetDisbursalAmount(netDisbursalAmount);
return loanOutstanding;
}
public void validateApproval(JsonCommand command, Long loanId) {
@@ -2074,7 +2076,9 @@ public final class LoanApplicationValidator {
StatusEnum.APPROVE.getValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());
if (loan.isTopup() && loan.getClientId() != null) {
validateTopupLoan(loan, expectedDisbursementDate);
final BigDecimal loanOutstanding = validateTopupLoan(loan, expectedDisbursementDate);
final BigDecimal netDisbursalAmountAdjusted = loan.getApprovedPrincipal().subtract(loanOutstanding);
loan.adjustNetDisbursalAmount(netDisbursalAmountAdjusted);
}
if (!loan.getStatus().isSubmittedAndPendingApproval()) {
@@ -2202,4 +2206,18 @@ public final class LoanApplicationValidator {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
private BigDecimal getFirstDisbursalAmount(final Loan loan) {
BigDecimal firstDisbursalAmount;
if (loan.isMultiDisburmentLoan()) {
List<DisbursementData> disbursementData = loanMapper.getDisbursementData(loan);
Collections.sort(disbursementData);
firstDisbursalAmount = disbursementData.get(disbursementData.size() - 1).getPrincipal();
} else {
firstDisbursalAmount = loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount();
}
return firstDisbursalAmount;
}
}
@@ -32,7 +32,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -60,6 +59,8 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeLoanTransactionDTO;
import org.apache.fineract.portfolio.loanaccount.data.AccrualBalances;
import org.apache.fineract.portfolio.loanaccount.data.AccrualChargeData;
import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodData;
@@ -79,6 +80,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepayme
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper;
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -109,6 +111,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
@Qualifier(TaskExecutorConstant.CONFIGURABLE_TASK_EXECUTOR_BEAN_NAME)
private final ThreadPoolTaskExecutor taskExecutor;
private final TransactionTemplate transactionTemplate;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
/**
* method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan
@@ -413,7 +416,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
if (!isFinal || progressiveAccrual) {
loan.setAccruedTill(isFinal ? accrualDate : tillDate);
}
ArrayList<Map<String, Object>> newTransactionMapping = new ArrayList<>();
List<AccountingBridgeLoanTransactionDTO> newTransactionDTOs = new ArrayList<>();
for (LoanTransaction accrualTransaction : accrualTransactions) {
accrualTransaction = loanTransactionRepository.saveAndFlush(accrualTransaction);
if (addJournal) {
@@ -421,11 +424,17 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
? new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction)
: new LoanAccrualAdjustmentTransactionBusinessEvent(accrualTransaction);
businessEventNotifierService.notifyPostBusinessEvent(businessEvent);
newTransactionMapping.add(accrualTransaction.toMapData(currency.getCode()));
AccountingBridgeLoanTransactionDTO transactionDTO = loanAccountingBridgeMapper.mapToLoanTransactionData(accrualTransaction,
currency.getCode());
newTransactionDTOs.add(transactionDTO);
}
}
if (addJournal) {
Map<String, Object> accountingBridgeData = deriveAccountingBridgeData(loan, newTransactionMapping);
AccountingBridgeDataDTO accountingBridgeData = new AccountingBridgeDataDTO(loan.getId(), loan.getLoanProduct().getId(),
loan.getOfficeId(), loan.getCurrencyCode(), loan.getSummary().getTotalInterestCharged(),
loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(), false,
false, false, null, newTransactionDTOs);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
}
@@ -1110,30 +1119,12 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
}
}
// Utility
private Map<String, Object> deriveAccountingBridgeData(@NotNull Loan loan, List<Map<String, Object>> newLoanTransactions) {
final Map<String, Object> accountingBridgeData = new LinkedHashMap<>();
accountingBridgeData.put("loanId", loan.getId());
accountingBridgeData.put("loanProductId", loan.getLoanProduct().getId());
accountingBridgeData.put("officeId", loan.getOfficeId());
accountingBridgeData.put("currencyCode", loan.getCurrencyCode());
accountingBridgeData.put("cashBasedAccountingEnabled", loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct());
accountingBridgeData.put("upfrontAccrualBasedAccountingEnabled", loan.isUpfrontAccrualAccountingEnabledOnLoanProduct());
accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", loan.isPeriodicAccrualAccountingEnabledOnLoanProduct());
accountingBridgeData.put("isAccountTransfer", false);
accountingBridgeData.put("isChargeOff", false);
accountingBridgeData.put("isFraud", false);
accountingBridgeData.put("newLoanTransactions", newLoanTransactions);
return accountingBridgeData;
}
private void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
final MonetaryCurrency currency = loan.getCurrency();
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer);
final AccountingBridgeDataDTO accountingBridgeData = loanAccountingBridgeMapper.deriveAccountingBridgeData(currency.getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, loan);
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
@@ -90,6 +90,7 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -128,6 +129,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanSc
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
@@ -178,6 +180,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanAccountService loanAccountService;
private final LoanAdjustmentService loanAdjustmentService;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate());
@@ -929,8 +932,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
loanAccountService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(loan.getCurrency().getCode(),
existingTransactionIds, existingReversedTransactionIds, false);
final AccountingBridgeDataDTO accountingBridgeData = loanAccountingBridgeMapper.deriveAccountingBridgeData(
loan.getCurrency().getCode(), existingTransactionIds, existingReversedTransactionIds, false, loan);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate);
@@ -1302,8 +1305,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
final MonetaryCurrency currency = loan.getCurrency();
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer);
final AccountingBridgeDataDTO accountingBridgeData = loanAccountingBridgeMapper.deriveAccountingBridgeData(currency.getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, loan);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
@@ -24,15 +24,18 @@ 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 jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
@@ -76,14 +79,14 @@ public class LoanDisbursementService {
if (disbursementDataArray != null && !disbursementDataArray.isEmpty()) {
String dateFormat;
Locale locale = null;
final Map<String, String> dateAndLocale = loan.getDateFormatAndLocale(jsonCommand);
final 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();
final Map<String, Object> parsedDisbursementData = loan.parseDisbursementDetails(jsonObject, dateFormat, locale);
final Map<String, Object> parsedDisbursementData = parseDisbursementDetails(jsonObject, dateFormat, locale);
final LocalDate expectedDisbursementDate = (LocalDate) parsedDisbursementData
.get(LoanApiConstants.expectedDisbursementDateParameterName);
final BigDecimal principal = (BigDecimal) parsedDisbursementData
@@ -298,4 +301,63 @@ public class LoanDisbursementService {
}) //
.forEach(loanCharge -> reprocessLoanTransactionsService.removeLoanCharge(loan, loanCharge));
}
// 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.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;
}
}
@@ -18,13 +18,13 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper;
import org.springframework.stereotype.Component;
@Component
@@ -32,23 +32,24 @@ import org.springframework.stereotype.Component;
public class LoanJournalEntryPoster {
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
public void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
final MonetaryCurrency currency = loan.getCurrency();
boolean isAccountTransfer = false;
List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
if (loan.isChargedOff()) {
accountingBridgeData = loan.deriveAccountingBridgeDataForChargeOff(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer);
} else {
accountingBridgeData.add(loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer));
}
for (Map<String, Object> accountingData : accountingBridgeData) {
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingData);
}
if (loan.isChargedOff()) {
List<AccountingBridgeDataDTO> accountingBridgeDataList = loanAccountingBridgeMapper.deriveAccountingBridgeDataForChargeOff(
currency.getCode(), existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, loan);
for (AccountingBridgeDataDTO accountingBridgeData : accountingBridgeDataList) {
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
} else {
AccountingBridgeDataDTO accountingBridgeData = loanAccountingBridgeMapper.deriveAccountingBridgeData(currency.getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, loan);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
}
}
@@ -38,7 +38,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
@@ -141,15 +141,13 @@ import org.springframework.data.domain.Pageable;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@AllArgsConstructor
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, LoanReadPlatformServiceCommon {
private static final String ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE = "submitted-date";
private final JdbcTemplate jdbcTemplate;
private final PlatformSecurityContext context;
private final LoanRepositoryWrapper loanRepositoryWrapper;
@@ -164,7 +162,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
private final CalendarReadPlatformService calendarReadPlatformService;
private final StaffReadPlatformService staffReadPlatformService;
private final PaginationHelper paginationHelper;
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
private final PaymentTypeReadPlatformService paymentTypeReadPlatformService;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final FloatingRatesReadPlatformService floatingRatesReadPlatformService;
@@ -179,6 +176,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
private final LoanTransactionRelationReadService loanTransactionRelationReadService;
private final LoanForeclosureValidator loanForeclosureValidator;
private final LoanTransactionMapper loanTransactionMapper;
private final org.apache.fineract.portfolio.loanaccount.mapper.LoanMapper loanMapper;
private final LoanTransactionProcessingService loadTransactionProcessingService;
@Override
@@ -149,6 +149,7 @@ import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.command.LoanUpdateCommand;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
@@ -163,7 +164,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachin
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
@@ -195,6 +195,8 @@ import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDoma
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanMapper;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
@@ -247,14 +249,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final FromJsonHelper fromApiJsonHelper;
private final CalendarRepository calendarRepository;
private final LoanScheduleHistoryWritePlatformService loanScheduleHistoryWritePlatformService;
private final LoanApplicationValidator loanApplicationCommandFromApiJsonHelper;
private final LoanApplicationValidator loanApplicationValidator;
private final AccountAssociationsRepository accountAssociationRepository;
private final AccountTransferDetailRepository accountTransferDetailRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final GuarantorDomainService guarantorDomainService;
private final LoanUtilService loanUtilService;
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessingStrategy;
private final CodeValueRepositoryWrapper codeValueRepository;
private final CashierTransactionDataValidator cashierTransactionDataValidator;
private final GLIMAccountInfoRepository glimRepository;
@@ -280,6 +281,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final LoanAccountService loanAccountService;
private final LoanJournalEntryPoster journalEntryPoster;
private final LoanAdjustmentService loanAdjustmentService;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
private final LoanMapper loanMapper;
private final LoanTransactionProcessingService loanTransactionProcessingService;
@Transactional
@@ -378,27 +381,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
if (loan.isTopup() && loan.getClientId() != null) {
final Long loanIdToClose = loan.getTopupLoanDetails().getLoanIdToClose();
final Loan loanToClose = this.loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, loan.getClientId());
if (loanToClose == null) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.to.be.closed.with.topup.is.not.active",
"Loan to be closed with this topup is not active.");
}
final LocalDate lastUserTransactionOnLoanToClose = loanToClose.getLastUserTransactionDate();
if (DateUtils.isBefore(loan.getDisbursementDate(), lastUserTransactionOnLoanToClose)) {
throw new GeneralPlatformDomainRuleException(
"error.msg.loan.disbursal.date.should.be.after.last.transaction.date.of.loan.to.be.closed",
"Disbursal date of this loan application " + loan.getDisbursementDate()
+ " should be after last transaction date of loan to be closed " + lastUserTransactionOnLoanToClose);
}
BigDecimal loanOutstanding = this.loanReadPlatformService
.retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, actualDisbursementDate).getAmount();
final BigDecimal firstDisbursalAmount = loan.getFirstDisbursalAmount();
if (loanOutstanding.compareTo(firstDisbursalAmount) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed",
"Topup loan amount should be greater than outstanding amount of loan to be closed.");
}
final BigDecimal loanOutstanding = loanApplicationValidator.validateTopupLoan(loan, actualDisbursementDate);
amountToDisburse = disburseAmount.minus(loanOutstanding);
disburseLoanToLoan(loan, command, loanOutstanding);
@@ -625,7 +608,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private void createAndSaveLoanScheduleArchive(final Loan loan, ScheduleGeneratorDTO scheduleGeneratorDTO) {
LoanRescheduleRequest loanRescheduleRequest = null;
LoanScheduleModel loanScheduleModel = loan.regenerateScheduleModel(scheduleGeneratorDTO);
LoanScheduleModel loanScheduleModel = loanMapper.regenerateScheduleModel(scheduleGeneratorDTO, loan);
List<LoanRepaymentScheduleInstallment> installments = retrieveRepaymentScheduleFromModel(loanScheduleModel);
this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(installments, loan, loanRescheduleRequest);
}
@@ -922,11 +905,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
this.accountTransfersWritePlatformService.reverseAllTransactions(loanId, PortfolioAccountType.LOAN);
String noteText;
createNote(loan, command, changes);
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer);
final AccountingBridgeDataDTO accountingBridgeData = loanAccountingBridgeMapper.deriveAccountingBridgeData(currency.getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, loan);
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoDisbursalBusinessEvent(loan));
@@ -2353,8 +2335,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("approvedLoanAmount", element);
loanApplicationCommandFromApiJsonHelper.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate,
principal);
loanApplicationValidator.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
@@ -2791,8 +2772,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
createNote(loan, command, changes);
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer);
final AccountingBridgeDataDTO accountingBridgeData = loanAccountingBridgeMapper.deriveAccountingBridgeData(currency.getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, loan);
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoLastDisbursalBusinessEvent(loan));
@@ -83,8 +83,10 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanChargeMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanCollateralManagementMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanTransactionMapper;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator;
@@ -162,7 +164,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@Configuration
public class LoanAccountConfiguration {
@@ -302,7 +303,8 @@ public class LoanAccountConfiguration {
LoanAccrualsProcessingService loanAccrualsProcessingService,
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanChargeValidator loanChargeValidator,
LoanScheduleService loanScheduleService, ReprocessLoanTransactionsService reprocessLoanTransactionsService,
LoanAccountService loanAccountService, LoanAdjustmentService loanAdjustmentService) {
LoanAccountService loanAccountService, LoanAdjustmentService loanAdjustmentService,
LoanAccountingBridgeMapper loanAccountingBridgeMapper) {
return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository,
businessEventNotifierService, loanTransactionRepository, accountTransfersWritePlatformService, loanRepositoryWrapper,
journalEntryWritePlatformService, loanAccountDomainService, loanChargeRepository, loanWritePlatformService, loanUtilService,
@@ -310,7 +312,8 @@ public class LoanAccountConfiguration {
configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory,
accountTransferDetailRepository, loanChargeAssembler, paymentDetailWritePlatformService, noteRepository,
loanAccrualTransactionBusinessEventService, loanAccrualsProcessingService, loanDownPaymentTransactionValidator,
loanChargeValidator, loanScheduleService, reprocessLoanTransactionsService, loanAccountService, loanAdjustmentService);
loanChargeValidator, loanScheduleService, reprocessLoanTransactionsService, loanAccountService, loanAdjustmentService,
loanAccountingBridgeMapper);
}
@Bean
@@ -322,23 +325,23 @@ public class LoanAccountConfiguration {
FundReadPlatformService fundReadPlatformService, ChargeReadPlatformService chargeReadPlatformService,
CodeValueReadPlatformService codeValueReadPlatformService, CalendarReadPlatformService calendarReadPlatformService,
StaffReadPlatformService staffReadPlatformService, PaginationHelper paginationHelper,
NamedParameterJdbcTemplate namedParameterJdbcTemplate, PaymentTypeReadPlatformService paymentTypeReadPlatformService,
PaymentTypeReadPlatformService paymentTypeReadPlatformService,
LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
FloatingRatesReadPlatformService floatingRatesReadPlatformService, LoanUtilService loanUtilService,
ConfigurationDomainService configurationDomainService, AccountDetailsReadPlatformService accountDetailsReadPlatformService,
ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator,
DelinquencyReadPlatformService delinquencyReadPlatformService, LoanTransactionRepository loanTransactionRepository,
LoanChargePaidByReadService loanChargePaidByReadService, LoanTransactionRelationReadService loanTransactionRelationReadService,
LoanForeclosureValidator loanForeclosureValidator, LoanTransactionMapper loanTransactionMapper,
LoanForeclosureValidator loanForeclosureValidator, LoanTransactionMapper loanTransactionMapper, LoanMapper loanMapper,
LoanTransactionProcessingService loanTransactionProcessingService) {
return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository,
loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService,
fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService,
staffReadPlatformService, paginationHelper, namedParameterJdbcTemplate, paymentTypeReadPlatformService,
staffReadPlatformService, paginationHelper, paymentTypeReadPlatformService,
loanRepaymentScheduleTransactionProcessorFactory, floatingRatesReadPlatformService, loanUtilService,
configurationDomainService, accountDetailsReadPlatformService, columnValidator, sqlGenerator,
delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService,
loanForeclosureValidator, loanTransactionMapper, loanTransactionProcessingService);
loanForeclosureValidator, loanTransactionMapper, loanMapper, loanTransactionProcessingService);
}
@Bean
@@ -387,9 +390,9 @@ public class LoanAccountConfiguration {
AccountTransferDetailRepository accountTransferDetailRepository, BusinessEventNotifierService businessEventNotifierService,
GuarantorDomainService guarantorDomainService, LoanUtilService loanUtilService,
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService,
LoanRepaymentScheduleTransactionProcessorFactory transactionProcessingStrategy, CodeValueRepositoryWrapper codeValueRepository,
CashierTransactionDataValidator cashierTransactionDataValidator, GLIMAccountInfoRepository glimRepository,
LoanRepository loanRepository, RepaymentWithPostDatedChecksAssembler repaymentWithPostDatedChecksAssembler,
CodeValueRepositoryWrapper codeValueRepository, CashierTransactionDataValidator cashierTransactionDataValidator,
GLIMAccountInfoRepository glimRepository, LoanRepository loanRepository,
RepaymentWithPostDatedChecksAssembler repaymentWithPostDatedChecksAssembler,
PostDatedChecksRepository postDatedChecksRepository,
LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository,
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccountLockService loanAccountLockService,
@@ -401,6 +404,7 @@ public class LoanAccountConfiguration {
LoanChargeValidator loanChargeValidator, LoanOfficerService loanOfficerService,
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanAccountService loanAccountService,
LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService,
LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper loanMapper,
LoanTransactionProcessingService loanTransactionProcessingService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
@@ -409,14 +413,14 @@ public class LoanAccountConfiguration {
accountTransfersWritePlatformService, accountTransfersReadPlatformService, accountAssociationsReadPlatformService,
loanReadPlatformService, fromApiJsonHelper, calendarRepository, loanScheduleHistoryWritePlatformService,
loanApplicationValidator, accountAssociationRepository, accountTransferDetailRepository, businessEventNotifierService,
guarantorDomainService, loanUtilService, entityDatatableChecksWritePlatformService, transactionProcessingStrategy,
codeValueRepository, cashierTransactionDataValidator, glimRepository, loanRepository, repaymentWithPostDatedChecksAssembler,
guarantorDomainService, loanUtilService, entityDatatableChecksWritePlatformService, codeValueRepository,
cashierTransactionDataValidator, glimRepository, loanRepository, repaymentWithPostDatedChecksAssembler,
postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, defaultLoanLifecycleStateMachine,
loanAccountLockService, externalIdFactory, loanAccrualTransactionBusinessEventService, errorHandler,
loanDownPaymentHandlerService, loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator,
loanDownPaymentTransactionValidator, loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService,
reprocessLoanTransactionsService, loanAccountService, journalEntryPoster, loanAdjustmentService,
loanTransactionProcessingService);
reprocessLoanTransactionsService, loanAccountService, journalEntryPoster, loanAdjustmentService, loanAccountingBridgeMapper,
loanMapper, loanTransactionProcessingService);
}
@Bean
@@ -461,10 +465,11 @@ public class LoanAccountConfiguration {
@Bean
@ConditionalOnMissingBean(LoanScheduleService.class)
public LoanScheduleService loanScheduleService(LoanChargeService loanChargeService,
ReprocessLoanTransactionsService reprocessLoanTransactionsService,
LoanTransactionProcessingService loanTransactionProcessingService, LoanScheduleComponent loanSchedule) {
return new LoanScheduleService(loanChargeService, reprocessLoanTransactionsService, loanTransactionProcessingService, loanSchedule);
public LoanScheduleService loanScheduleService(final LoanChargeService loanChargeService,
final ReprocessLoanTransactionsService reprocessLoanTransactionsService, final LoanMapper loanMapper,
final LoanTransactionProcessingService loanTransactionProcessingService, LoanScheduleComponent loanSchedule) {
return new LoanScheduleService(loanChargeService, reprocessLoanTransactionsService, loanMapper, loanTransactionProcessingService,
loanSchedule);
}
@Bean
@@ -18,10 +18,11 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;
import static org.mockito.Mockito.any;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import java.time.LocalDate;
@@ -89,7 +90,7 @@ public class LoanAccrualsProcessingServiceImplTest {
verify(loan, times(1)).isClosed();
verify(loanTransactionRepository, never()).saveAndFlush(any());
verify(journalEntryWritePlatformService, never()).createJournalEntriesForLoan(any());
verifyNoInteractions(journalEntryWritePlatformService);
verify(businessEventNotifierService, never()).notifyPostBusinessEvent(any());
verify(loan, never()).addLoanTransaction(any());
}
@@ -19,7 +19,11 @@
package org.apache.fineract.portfolio.loanaccount.service;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -27,6 +31,8 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
@@ -38,6 +44,7 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountService;
@@ -46,6 +53,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
@@ -71,6 +79,7 @@ class LoanChargeWritePlatformServiceImplTest {
private static final LocalDate BUSINESS_DATE_AFTER = LocalDate.of(2024, 2, 26);
private static final LocalDate BUSINESS_DATE_ON = MATURITY_DATE;
private static final LocalDate BUSINESS_DATE_BEFORE = LocalDate.of(2024, 2, 14);
private static final String CURRENCY_CODE = "USD";
@InjectMocks
private LoanChargeWritePlatformServiceImpl loanChargeWritePlatformService;
@@ -138,19 +147,35 @@ class LoanChargeWritePlatformServiceImplTest {
@Mock
private LoanAccountService loanAccountService;
@Mock
private LoanAccountingBridgeMapper loanAccountingBridgeMapper;
@BeforeEach
void setUp() {
when(loanAssembler.assembleFrom(LOAN_ID)).thenReturn(loan);
when(chargeRepository.findOneWithNotFoundDetection(anyLong())).thenReturn(chargeDefinition);
when(chargeDefinition.getChargeTimeType()).thenReturn(SPECIFIED_DUE_DATE);
when(chargeDefinition.getCurrencyCode()).thenReturn(CURRENCY_CODE);
when(loanChargeAssembler.createNewFromJson(loan, chargeDefinition, jsonCommand)).thenReturn(loanCharge);
when(loan.repaymentScheduleDetail()).thenReturn(loanRepaymentScheduleDetail);
when(loan.hasCurrencyCodeOf(any())).thenReturn(true);
when(loan.hasCurrencyCodeOf(CURRENCY_CODE)).thenReturn(true);
when(loanCharge.getChargePaymentMode()).thenReturn(ChargePaymentMode.REGULAR);
when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
when(loanChargeRepository.saveAndFlush(any(LoanCharge.class))).thenReturn(loanCharge);
when(loan.getCurrency()).thenReturn(monetaryCurrency);
when(monetaryCurrency.getCode()).thenReturn(CURRENCY_CODE);
when(loanAccountService.saveAndFlushLoanWithDataIntegrityViolationChecks(any())).thenReturn(loan);
List<Long> existingTransactionIds = new ArrayList<>();
List<Long> existingReversedTransactionIds = new ArrayList<>();
when(loan.findExistingTransactionIds()).thenReturn(existingTransactionIds);
when(loan.findExistingReversedTransactionIds()).thenReturn(existingReversedTransactionIds);
when(loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()).thenReturn(false);
when(loan.isCashBasedAccountingEnabledOnLoanProduct()).thenReturn(false);
when(loan.isUpfrontAccrualAccountingEnabledOnLoanProduct()).thenReturn(false);
when(loanAccountingBridgeMapper.deriveAccountingBridgeData(anyString(), anyList(), anyList(), anyBoolean(), any(Loan.class))).thenReturn(new AccountingBridgeDataDTO());
doNothing().when(journalEntryWritePlatformService).createJournalEntriesForLoan(any(AccountingBridgeDataDTO.class));
}
@ParameterizedTest
@@ -160,6 +185,10 @@ class LoanChargeWritePlatformServiceImplTest {
when(loan.getMaturityDate()).thenReturn(maturityDate);
when(loan.handleChargeAppliedTransaction(loanCharge, null)).thenReturn(loanTransaction);
if (isAccrualExpected) {
when(loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()).thenReturn(true);
}
try (MockedStatic<DateUtils> mockedDateUtils = mockStatic(DateUtils.class)) {
mockedDateUtils.when(DateUtils::getBusinessLocalDate).thenReturn(businessDate);