FINERACT-2090: Restructure Loan creation and modification

This commit is contained in:
Adam Saghy
2024-06-06 23:09:50 +02:00
parent a078cfd8c5
commit 92e291a9e4
34 changed files with 2458 additions and 2486 deletions
@@ -20,7 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.api;
public interface LoanApiConstants {
String emiAmountParameterName = "fixedEmiAmount";
String fixedEmiAmountParameterName = "fixedEmiAmount";
String maxOutstandingBalanceParameterName = "maxOutstandingLoanBalance";
String disbursementDataParameterName = "disbursementData";
String expectedDisbursementDateParameterName = "expectedDisbursementDate";
@@ -36,6 +36,7 @@ public interface LoanApiConstants {
String amountParameterName = "amount";
String chargeTimeTypeParameterName = "chargeTimeType";
String chargeCalculationTypeParameterName = "chargeCalculationType";
String chargePaymentModeParameterName = "chargePaymentMode";
String principalDisbursedParameterName = "transactionAmount";
String chargesParameterName = "charges";
String loanIdTobeApproved = "loanId";
@@ -18,44 +18,30 @@
*/
package org.apache.fineract.portfolio.loanaccount.command;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Data;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
/**
* Java object representation of {@link LoanCharge} API JSON.
*/
public class LoanChargeCommand implements Comparable<LoanChargeCommand> {
@Data
public class LoanChargeCommand implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@SuppressWarnings("unused")
private final Long id;
private final Long chargeId;
private final BigDecimal amount;
private Long id;
private Long chargeId;
private BigDecimal amount;
@SuppressWarnings("unused")
private final Integer chargeTimeType;
private Integer chargeTimeType;
@SuppressWarnings("unused")
private final Integer chargeCalculationType;
private Integer chargeCalculationType;
@SuppressWarnings("unused")
private final LocalDate dueDate;
public LoanChargeCommand(final Long id, final Long chargeId, final BigDecimal amount, final Integer chargeTimeType,
final Integer chargeCalculationType, final LocalDate dueDate) {
this.id = id;
this.chargeId = chargeId;
this.amount = amount;
this.chargeTimeType = chargeTimeType;
this.chargeCalculationType = chargeCalculationType;
this.dueDate = dueDate;
}
@Override
@SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS", justification = "TODO: fix this! See: https://stackoverflow.com/questions/2609037/findbugs-how-to-solve-eq-compareto-use-object-equals")
public int compareTo(final LoanChargeCommand o) {
int comparison = this.chargeId.compareTo(o.chargeId);
if (comparison == 0) {
comparison = this.amount.compareTo(o.amount);
}
return comparison;
}
private LocalDate dueDate;
}
@@ -19,20 +19,20 @@
package org.apache.fineract.portfolio.loanaccount.data;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Data;
@RequiredArgsConstructor
@Getter
@Data
@AllArgsConstructor
public class LoanCollateralManagementData {
private final Long clientCollateralId;
private Long clientCollateralId;
private final BigDecimal quantity;
private BigDecimal quantity;
private final BigDecimal total;
private BigDecimal total;
private final BigDecimal totalCollateral;
private BigDecimal totalCollateral;
private final Long id;
private Long id;
}
@@ -60,6 +60,8 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import lombok.Getter;
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;
@@ -104,10 +106,8 @@ import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData;
import org.apache.fineract.portfolio.fund.domain.Fund;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.command.LoanChargeCommand;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
@@ -128,7 +128,6 @@ import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementData
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
import org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbursementException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
@@ -153,6 +152,8 @@ import org.apache.fineract.useradministration.domain.AppUser;
@Entity
@Table(name = "m_loan", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "loan_account_no_UNIQUE"),
@UniqueConstraint(columnNames = { "external_id" }, name = "loan_externalid_UNIQUE") })
@Setter
@Getter
public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public static final String RECALCULATE_LOAN_SCHEDULE = "recalculateLoanSchedule";
@@ -475,62 +476,73 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
@Column(name = "enable_installment_level_delinquency", nullable = false)
private boolean enableInstallmentLevelDelinquency = false;
public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType,
public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final AccountType loanType,
final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final String transactionProcessingStrategyCode, final LoanProductRelatedDetail loanRepaymentScheduleDetail,
final Set<LoanCharge> loanCharges, final Set<LoanCollateralManagement> collateral, final BigDecimal fixedEmiAmount,
final LoanRepaymentScheduleTransactionProcessor transactionProcessingStrategy,
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges,
final Set<LoanCollateralManagement> collateral, final BigDecimal fixedEmiAmount,
final List<LoanDisbursementDetails> disbursementDetails, final BigDecimal maxOutstandingLoanBalance,
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment) {
return new Loan(accountNo, client, null, loanType, fund, officer, loanPurpose, transactionProcessingStrategyCode, loanProduct,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel,
final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) {
return new Loan(accountNo, client, null, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, null, loanCharges, collateral, null, fixedEmiAmount, disbursementDetails,
maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment);
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel,
enableInstallmentLevelDelinquency, submittedOnDate);
}
public static Loan newGroupLoanApplication(final String accountNo, final Group group, final Integer loanType,
public static Loan newGroupLoanApplication(final String accountNo, final Group group, final AccountType loanType,
final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final String transactionProcessingStrategyCode, final LoanProductRelatedDetail loanRepaymentScheduleDetail,
final Set<LoanCharge> loanCharges, final Set<LoanCollateralManagement> collateral, final Boolean syncDisbursementWithMeeting,
final BigDecimal fixedEmiAmount, final List<LoanDisbursementDetails> disbursementDetails,
final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement,
final Boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final List<Rate> rates,
final BigDecimal fixedPrincipalPercentagePerInstallment) {
return new Loan(accountNo, null, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategyCode, loanProduct,
loanRepaymentScheduleDetail, null, loanCharges, collateral, syncDisbursementWithMeeting, fixedEmiAmount,
disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate,
interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment);
final LoanRepaymentScheduleTransactionProcessor transactionProcessingStrategy,
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges,
final Boolean syncDisbursementWithMeeting, final BigDecimal fixedEmiAmount,
final List<LoanDisbursementDetails> disbursementDetails, final BigDecimal maxOutstandingLoanBalance,
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel,
final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) {
return new Loan(accountNo, null, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, null, loanCharges, null, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails,
maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel,
enableInstallmentLevelDelinquency, submittedOnDate);
}
public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group,
final Integer loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final String transactionProcessingStrategyCode, final LoanProductRelatedDetail loanRepaymentScheduleDetail,
final Set<LoanCharge> loanCharges, final Set<LoanCollateralManagement> collateral, final Boolean syncDisbursementWithMeeting,
final BigDecimal fixedEmiAmount, final List<LoanDisbursementDetails> disbursementDetails,
final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement,
final Boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final List<Rate> rates,
final BigDecimal fixedPrincipalPercentagePerInstallment) {
return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategyCode, loanProduct,
loanRepaymentScheduleDetail, null, loanCharges, collateral, syncDisbursementWithMeeting, fixedEmiAmount,
disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate,
interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment);
final AccountType loanType, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final LoanRepaymentScheduleTransactionProcessor transactionProcessingStrategy,
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges,
final Boolean syncDisbursementWithMeeting, final BigDecimal fixedEmiAmount,
final List<LoanDisbursementDetails> disbursementDetails, final BigDecimal maxOutstandingLoanBalance,
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel,
final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) {
return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, null, loanCharges, null, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails,
maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel,
enableInstallmentLevelDelinquency, submittedOnDate);
}
protected Loan() {
this.client = null;
}
private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund,
final Staff loanOfficer, final CodeValue loanPurpose, final String transactionProcessingStrategyCode,
final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus,
final Set<LoanCharge> loanCharges, final Set<LoanCollateralManagement> collateral, final Boolean syncDisbursementWithMeeting,
final BigDecimal fixedEmiAmount, final List<LoanDisbursementDetails> disbursementDetails,
final BigDecimal maxOutstandingLoanBalance, final Boolean createStandingInstructionAtDisbursement,
final Boolean isFloatingInterestRate, final BigDecimal interestRateDifferential, final List<Rate> rates,
final BigDecimal fixedPrincipalPercentagePerInstallment) {
private Loan(final String accountNo, final Client client, final Group group, final AccountType loanType, final Fund fund,
final Staff loanOfficer, final CodeValue loanPurpose,
final LoanRepaymentScheduleTransactionProcessor transactionProcessingStrategy, final LoanProduct loanProduct,
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus, final Set<LoanCharge> loanCharges,
final Set<LoanCollateralManagement> collateral, final Boolean syncDisbursementWithMeeting, final BigDecimal fixedEmiAmount,
final List<LoanDisbursementDetails> disbursementDetails, final BigDecimal maxOutstandingLoanBalance,
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel,
final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) {
this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail;
this.loanRepaymentScheduleDetail.validateRepaymentPeriodWithGraceSettings();
this.isFloatingInterestRate = isFloatingInterestRate;
this.interestRateDifferential = interestRateDifferential;
@@ -543,12 +555,14 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
}
this.client = client;
this.group = group;
this.loanType = loanType;
this.loanType = loanType.getValue();
this.fund = fund;
this.loanOfficer = loanOfficer;
this.loanPurpose = loanPurpose;
this.transactionProcessingStrategyCode = transactionProcessingStrategyCode;
this.transactionProcessingStrategyCode = transactionProcessingStrategy.getCode();
this.transactionProcessingStrategyName = transactionProcessingStrategy.getName();
this.loanProduct = loanProduct;
if (loanStatus != null) {
this.loanStatus = loanStatus.getValue();
@@ -563,7 +577,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
this.summary = new LoanSummary();
}
if (loanType.equals(1) && collateral != null && !collateral.isEmpty()) {
if (loanType.isIndividualAccount() && collateral != null && !collateral.isEmpty()) {
this.loanCollateralManagements = associateWithThisLoan(collateral);
} else {
this.loanCollateralManagements = null;
@@ -573,7 +587,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
this.syncDisbursementWithMeeting = syncDisbursementWithMeeting;
this.fixedEmiAmount = fixedEmiAmount;
this.maxOutstandingLoanBalance = maxOutstandingLoanBalance;
this.disbursementDetails = disbursementDetails;
if (disbursementDetails != null) {
this.disbursementDetails = disbursementDetails;
for (final LoanDisbursementDetails loanDisbursementDetails : disbursementDetails) {
loanDisbursementDetails.updateLoan(this);
}
}
this.approvedPrincipal = this.loanRepaymentScheduleDetail.getPrincipal().getAmount();
this.createStandingInstructionAtDisbursement = createStandingInstructionAtDisbursement;
@@ -590,7 +609,26 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
// Add net get net disbursal amount from charges and principal
this.netDisbursalAmount = this.approvedPrincipal.subtract(deriveSumTotalOfChargesDueAtDisbursement());
this.loanStatus = LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue();
this.externalId = externalId;
this.termFrequency = loanApplicationTerms.getLoanTermFrequency();
this.termPeriodFrequencyType = loanApplicationTerms.getLoanTermPeriodFrequencyType().getValue();
this.expectedDisbursementDate = loanApplicationTerms.getExpectedDisbursementDate();
this.expectedFirstRepaymentOnDate = loanApplicationTerms.getRepaymentStartFromDate();
this.interestChargedFromDate = loanApplicationTerms.getInterestChargedFromDate();
this.submittedOnDate = submittedOnDate != null ? submittedOnDate : DateUtils.getBusinessLocalDate();
updateLoanSchedule(loanScheduleModel);
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
// Copy interest recalculation settings if interest recalculation is enabled
if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) {
this.loanInterestRecalculationDetails = LoanInterestRecalculationDetails
.createFrom(this.loanProduct.getProductInterestRecalculationDetails());
this.loanInterestRecalculationDetails.updateLoan(this);
}
this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency;
}
public Integer getNumberOfRepayments() {
@@ -652,10 +690,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return this.accountNumberRequiresAutoGeneration;
}
public void setAccountNumberRequiresAutoGeneration(final boolean accountNumberRequiresAutoGeneration) {
this.accountNumberRequiresAutoGeneration = accountNumberRequiresAutoGeneration;
}
public void addLoanCharge(final LoanCharge loanCharge) {
if (isChargesAdditionAllowed() && loanCharge.isDueAtDisbursement()) {
@@ -1460,274 +1494,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
doPostLoanTransactionChecks(getLastUserTransactionDate(), loanLifecycleStateMachine);
}
public Map<String, Object> loanApplicationModification(final JsonCommand command, final Set<LoanCharge> possiblyModifedLoanCharges,
final Set<LoanCollateralManagement> possiblyModifiedLoanCollateralItems, final AprCalculator aprCalculator,
boolean isChargesModified, final LoanProduct loanProduct) {
final Map<String, Object> actualChanges = this.loanRepaymentScheduleDetail.updateLoanApplicationAttributes(command, aprCalculator);
final MonetaryCurrency currency = new MonetaryCurrency(loanProduct.getCurrency().getCode(),
loanProduct.getCurrency().getDigitsAfterDecimal(), loanProduct.getCurrency().getCurrencyInMultiplesOf());
this.loanRepaymentScheduleDetail.updateCurrency(currency);
if (!actualChanges.isEmpty()) {
final boolean recalculateLoanSchedule = !(actualChanges.size() == 1 && actualChanges.containsKey(IN_ARREARS_TOLERANCE));
actualChanges.put(RECALCULATE_LOAN_SCHEDULE, recalculateLoanSchedule);
isChargesModified = true;
}
final String dateFormatAsInput = command.dateFormat();
final String localeAsInput = command.locale();
final String accountNoParamName = ACCOUNT_NO;
if (command.isChangeInStringParameterNamed(accountNoParamName, this.accountNumber)) {
final String newValue = command.stringValueOfParameterNamed(accountNoParamName);
actualChanges.put(accountNoParamName, newValue);
this.accountNumber = StringUtils.defaultIfEmpty(newValue, null);
}
if (command.isChangeInBooleanParameterNamed(CREATE_STANDING_INSTRUCTION_AT_DISBURSEMENT,
shouldCreateStandingInstructionAtDisbursement())) {
final Boolean valueAsInput = command.booleanObjectValueOfParameterNamed(CREATE_STANDING_INSTRUCTION_AT_DISBURSEMENT);
actualChanges.put(CREATE_STANDING_INSTRUCTION_AT_DISBURSEMENT, valueAsInput);
this.createStandingInstructionAtDisbursement = valueAsInput;
}
if (command.isChangeInStringParameterNamed(EXTERNAL_ID, this.externalId.getValue())) {
final String newValue = command.stringValueOfParameterNamed(EXTERNAL_ID);
ExternalId externalId = ExternalIdFactory.produce(newValue);
if (externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) {
externalId = ExternalId.generate();
}
actualChanges.put(EXTERNAL_ID, externalId);
this.externalId = externalId;
}
// add clientId, groupId and loanType changes to actual changes
final Long clientId = this.client == null ? null : this.client.getId();
if (command.isChangeInLongParameterNamed(CLIENT_ID, clientId)) {
final Long newValue = command.longValueOfParameterNamed(CLIENT_ID);
actualChanges.put(CLIENT_ID, newValue);
}
// FIXME: AA - We may require separate api command to move loan from one
// group to another
final Long groupId = this.group == null ? null : this.group.getId();
if (command.isChangeInLongParameterNamed(GROUP_ID, groupId)) {
final Long newValue = command.longValueOfParameterNamed(GROUP_ID);
actualChanges.put(GROUP_ID, newValue);
}
if (command.isChangeInLongParameterNamed(PRODUCT_ID, this.loanProduct.getId())) {
final Long newValue = command.longValueOfParameterNamed(PRODUCT_ID);
actualChanges.put(PRODUCT_ID, newValue);
actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true);
}
if (command.isChangeInBooleanParameterNamed(IS_FLOATING_INTEREST_RATE, this.isFloatingInterestRate)) {
final Boolean newValue = command.booleanObjectValueOfParameterNamed(IS_FLOATING_INTEREST_RATE);
actualChanges.put(IS_FLOATING_INTEREST_RATE, newValue);
this.isFloatingInterestRate = newValue;
}
if (command.isChangeInBigDecimalParameterNamed(INTEREST_RATE_DIFFERENTIAL, this.interestRateDifferential)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(INTEREST_RATE_DIFFERENTIAL);
actualChanges.put(INTEREST_RATE_DIFFERENTIAL, newValue);
this.interestRateDifferential = newValue;
}
Long existingFundId = null;
if (this.fund != null) {
existingFundId = this.fund.getId();
}
if (command.isChangeInLongParameterNamed(FUND_ID, existingFundId)) {
final Long newValue = command.longValueOfParameterNamed(FUND_ID);
actualChanges.put(FUND_ID, newValue);
}
Long existingLoanOfficerId = null;
if (this.loanOfficer != null) {
existingLoanOfficerId = this.loanOfficer.getId();
}
if (command.isChangeInLongParameterNamed(LOAN_OFFICER_ID, existingLoanOfficerId)) {
final Long newValue = command.longValueOfParameterNamed(LOAN_OFFICER_ID);
actualChanges.put(LOAN_OFFICER_ID, newValue);
}
Long existingLoanPurposeId = null;
if (this.loanPurpose != null) {
existingLoanPurposeId = this.loanPurpose.getId();
}
if (command.isChangeInLongParameterNamed(LOAN_PURPOSE_ID, existingLoanPurposeId)) {
final Long newValue = command.longValueOfParameterNamed(LOAN_PURPOSE_ID);
actualChanges.put(LOAN_PURPOSE_ID, newValue);
}
if (command.isChangeInStringParameterNamed(TRANSACTION_PROCESSING_STRATEGY_CODE, transactionProcessingStrategyCode)) {
final String newValue = command.stringValueOfParameterNamed(TRANSACTION_PROCESSING_STRATEGY_CODE);
actualChanges.put(TRANSACTION_PROCESSING_STRATEGY_CODE, newValue);
}
if (command.isChangeInLocalDateParameterNamed(SUBMITTED_ON_DATE, getSubmittedOnDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(SUBMITTED_ON_DATE);
actualChanges.put(SUBMITTED_ON_DATE, valueAsInput);
actualChanges.put(DATE_FORMAT, dateFormatAsInput);
actualChanges.put(LOCALE, localeAsInput);
this.submittedOnDate = command.localDateValueOfParameterNamed(SUBMITTED_ON_DATE);
}
if (command.isChangeInLocalDateParameterNamed(EXPECTED_DISBURSEMENT_DATE, getExpectedDisbursedOnLocalDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(EXPECTED_DISBURSEMENT_DATE);
actualChanges.put(EXPECTED_DISBURSEMENT_DATE, valueAsInput);
actualChanges.put(DATE_FORMAT, dateFormatAsInput);
actualChanges.put(LOCALE, localeAsInput);
actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true);
this.expectedDisbursementDate = command.localDateValueOfParameterNamed(EXPECTED_DISBURSEMENT_DATE);
removeFirstDisbursementTransaction();
}
if (command.isChangeInLocalDateParameterNamed(REPAYMENTS_STARTING_FROM_DATE, getExpectedFirstRepaymentOnDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(REPAYMENTS_STARTING_FROM_DATE);
actualChanges.put(REPAYMENTS_STARTING_FROM_DATE, valueAsInput);
actualChanges.put(DATE_FORMAT, dateFormatAsInput);
actualChanges.put(LOCALE, localeAsInput);
actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true);
this.expectedFirstRepaymentOnDate = command.localDateValueOfParameterNamed(REPAYMENTS_STARTING_FROM_DATE);
}
if (command.isChangeInBooleanParameterNamed(SYNC_DISBURSEMENT_WITH_MEETING, isSyncDisbursementWithMeeting())) {
final Boolean valueAsInput = command.booleanObjectValueOfParameterNamed(SYNC_DISBURSEMENT_WITH_MEETING);
actualChanges.put(SYNC_DISBURSEMENT_WITH_MEETING, valueAsInput);
this.syncDisbursementWithMeeting = valueAsInput;
}
if (command.isChangeInLocalDateParameterNamed(INTEREST_CHARGED_FROM_DATE, getInterestChargedFromDate())) {
final String valueAsInput = command.stringValueOfParameterNamed(INTEREST_CHARGED_FROM_DATE);
actualChanges.put(INTEREST_CHARGED_FROM_DATE, valueAsInput);
actualChanges.put(DATE_FORMAT, dateFormatAsInput);
actualChanges.put(LOCALE, localeAsInput);
actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true);
this.interestChargedFromDate = command.localDateValueOfParameterNamed(INTEREST_CHARGED_FROM_DATE);
}
// the comparison should be done with the tenant date
// (DateUtils.getBusinessDate()) and not the server date (new
// LocalDate())
if (DateUtils.isDateInTheFuture(getSubmittedOnDate())) {
final String errorMessage = "The date on which a loan is submitted cannot be in the future.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, getSubmittedOnDate());
}
if (this.client != null && DateUtils.isBefore(getSubmittedOnDate(), this.client.getActivationDate())) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage,
getSubmittedOnDate());
} else if (this.group != null && DateUtils.isBefore(getSubmittedOnDate(), this.group.getActivationDate())) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage,
getSubmittedOnDate());
}
if (DateUtils.isAfter(getSubmittedOnDate(), getExpectedDisbursedOnLocalDate())) {
final String errorMessage = "The date on which a loan is submitted cannot be after its expected disbursement date: "
+ getExpectedDisbursedOnLocalDate().toString();
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.after.expected.disbursement.date", errorMessage,
getSubmittedOnDate(), getExpectedDisbursedOnLocalDate());
}
if (isChargesModified) {
actualChanges.put(PARAM_CHARGES, getLoanCharges(possiblyModifedLoanCharges));
actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true);
}
if (command.parameterExists(PARAM_COLLATERAL) && possiblyModifiedLoanCollateralItems != null
&& !possiblyModifiedLoanCollateralItems.isEmpty()) {
Set<LoanCollateralManagement> loanCollateralManagements = this.loanCollateralManagements;
boolean isTrue = possiblyModifiedLoanCollateralItems.equals(loanCollateralManagements);
if (!isTrue) {
actualChanges.put(PARAM_COLLATERAL, getLoanCollateralDataFormCommand(possiblyModifiedLoanCollateralItems));
}
}
if (command.isChangeInIntegerParameterNamed(LOAN_TERM_FREQUENCY, this.termFrequency)) {
final Integer newValue = command.integerValueOfParameterNamed(LOAN_TERM_FREQUENCY);
actualChanges.put(LOAN_TERM_FREQUENCY, newValue);
this.termFrequency = newValue;
}
if (command.isChangeInIntegerParameterNamed(LOAN_TERM_FREQUENCY_TYPE, this.termPeriodFrequencyType)) {
final Integer newValue = command.integerValueOfParameterNamed(LOAN_TERM_FREQUENCY_TYPE);
final PeriodFrequencyType newTermPeriodFrequencyType = PeriodFrequencyType.fromInt(newValue);
actualChanges.put(LOAN_TERM_FREQUENCY_TYPE, newTermPeriodFrequencyType.getValue());
this.termPeriodFrequencyType = newValue;
}
if (command.isChangeInBigDecimalParameterNamed(PRINCIPAL, this.approvedPrincipal)) {
this.approvedPrincipal = command.bigDecimalValueOfParameterNamed(PRINCIPAL);
}
if (command.isChangeInBigDecimalParameterNamed(PRINCIPAL, this.proposedPrincipal)) {
this.proposedPrincipal = command.bigDecimalValueOfParameterNamed(PRINCIPAL);
}
if (loanProduct.isMultiDisburseLoan()) {
updateDisbursementDetails(command, actualChanges);
if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName,
this.maxOutstandingLoanBalance)) {
this.maxOutstandingLoanBalance = command
.bigDecimalValueOfParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName);
}
final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName);
if (loanProduct.isDisallowExpectedDisbursements()) {
if (disbursementDataArray != null && !disbursementDataArray.isEmpty()) {
final String errorMessage = "For this loan product, disbursement details are not allowed";
throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
} else {
if (disbursementDataArray == null || disbursementDataArray.size() == 0) {
final String errorMessage = "For this loan product, disbursement details must be provided";
throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
if (disbursementDataArray.size() > loanProduct.maxTrancheCount()) {
final String errorMessage = "Number of tranche shouldn't be greter than " + loanProduct.maxTrancheCount();
throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage,
loanProduct.maxTrancheCount(), disbursementDetails.size());
}
}
} else {
this.disbursementDetails.clear();
}
if (loanProduct.isMultiDisburseLoan() || loanProduct.isCanDefineInstallmentAmount()) {
if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.emiAmountParameterName, this.fixedEmiAmount)) {
this.fixedEmiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.emiAmountParameterName);
actualChanges.put(LoanApiConstants.emiAmountParameterName, this.fixedEmiAmount);
actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true);
}
} else {
this.fixedEmiAmount = null;
}
if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName,
this.fixedPrincipalPercentagePerInstallment)) {
this.fixedPrincipalPercentagePerInstallment = command
.bigDecimalValueOfParameterNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName);
actualChanges.put(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName,
this.fixedPrincipalPercentagePerInstallment);
}
return actualChanges;
}
public void recalculateAllCharges() {
Set<LoanCharge> charges = this.getActiveCharges();
int penaltyWaitPeriod = 0;
@@ -2068,112 +1834,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return list;
}
private LoanCollateralManagementData[] getLoanCollateralDataFormCommand(final Set<LoanCollateralManagement> setOfLoanCollateral) {
LoanCollateralManagementData[] existingLoanCollateral = null;
final List<LoanCollateralManagementData> loanCollateralList = new ArrayList<>();
for (final LoanCollateralManagement loanCollateral : setOfLoanCollateral) {
loanCollateralList.add(loanCollateral.toCommand());
}
existingLoanCollateral = loanCollateralList.toArray(new LoanCollateralManagementData[0]);
return existingLoanCollateral;
}
private LoanChargeCommand[] getLoanCharges(final Set<LoanCharge> setOfLoanCharges) {
LoanChargeCommand[] existingLoanCharges = null;
final List<LoanChargeCommand> loanChargesList = new ArrayList<>();
for (final LoanCharge loanCharge : setOfLoanCharges) {
loanChargesList.add(loanCharge.toCommand());
}
existingLoanCharges = loanChargesList.toArray(new LoanChargeCommand[0]);
return existingLoanCharges;
}
private void removeFirstDisbursementTransaction() {
List<LoanTransaction> transactions = getLoanTransactions();
for (final LoanTransaction loanTransaction : transactions) {
if (loanTransaction.isDisbursement()) {
removeLoanTransaction(loanTransaction);
break;
}
}
}
public void loanApplicationSubmittal(final LoanScheduleModel loanSchedule, final LoanApplicationTerms loanApplicationTerms,
final LoanLifecycleStateMachine lifecycleStateMachine, final LocalDate submittedOn, final ExternalId externalId,
final boolean allowTransactionsOnHoliday, final List<Holiday> holidays, final WorkingDays workingDays,
final boolean allowTransactionsOnNonWorkingDay) {
updateLoanSchedule(loanSchedule);
lifecycleStateMachine.transition(LoanEvent.LOAN_CREATED, this);
this.externalId = externalId;
this.termFrequency = loanApplicationTerms.getLoanTermFrequency();
this.termPeriodFrequencyType = loanApplicationTerms.getLoanTermPeriodFrequencyType().getValue();
this.submittedOnDate = submittedOn;
this.expectedDisbursementDate = loanApplicationTerms.getExpectedDisbursementDate();
this.expectedFirstRepaymentOnDate = loanApplicationTerms.getRepaymentStartFromDate();
this.interestChargedFromDate = loanApplicationTerms.getInterestChargedFromDate();
updateLoanScheduleDependentDerivedFields();
if (DateUtils.isDateInTheFuture(submittedOn)) {
final String errorMessage = "The date on which a loan is submitted cannot be in the future.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn,
DateUtils.getBusinessLocalDate());
}
if (this.client != null && this.client.isActivatedAfter(submittedOn)) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, submittedOn,
client.getActivationDate());
}
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_CREATED, submittedOn);
if (this.group != null && this.group.isActivatedAfter(submittedOn)) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date.";
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, submittedOn,
group.getActivationDate());
}
if (DateUtils.isAfter(submittedOn, getExpectedDisbursedOnLocalDate())) {
final String errorMessage = "The date on which a loan is submitted cannot be after its expected disbursement date: "
+ getExpectedDisbursedOnLocalDate().toString();
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.after.expected.disbursement.date", errorMessage,
submittedOn, getExpectedDisbursedOnLocalDate());
}
// charges are optional
int penaltyWaitPeriod = 0;
for (final LoanCharge loanCharge : getActiveCharges()) {
recalculateLoanCharge(loanCharge, penaltyWaitPeriod);
}
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
// validate if disbursement date is a holiday or a non-working day
validateDisbursementDateIsOnNonWorkingDay(workingDays, allowTransactionsOnNonWorkingDay);
validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays);
// Copy interest recalculation settings if interest recalculation is enabled
if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) {
this.loanInterestRecalculationDetails = LoanInterestRecalculationDetails
.createFrom(this.loanProduct.getProductInterestRecalculationDetails());
this.loanInterestRecalculationDetails.updateLoan(this);
}
}
private LocalDate determineExpectedMaturityDate() {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments().stream()
.filter(i -> !i.isDownPayment() && !i.isAdditional()).toList();
@@ -4876,7 +4536,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
break;
}
return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from
// startDate
// startDate
}
private void validateDisbursementDateIsOnNonWorkingDay(final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) {
@@ -4945,7 +4605,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
}
public void updateInterestRateFrequencyType() {
this.loanRepaymentScheduleDetail.updateInterestPeriodFrequencyType(this.loanProduct.getInterestPeriodFrequencyType());
this.loanRepaymentScheduleDetail.setInterestPeriodFrequencyType(this.loanProduct.getInterestPeriodFrequencyType());
}
public Integer getTermFrequency() {
@@ -4975,14 +4635,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
this.loanStatus = loanStatus;
}
public void validateExpectedDisbursementForHolidayAndNonWorkingDay(final WorkingDays workingDays,
final boolean allowTransactionsOnHoliday, final List<Holiday> holidays, final boolean allowTransactionsOnNonWorkingDay) {
// validate if disbursement date is a holiday or a non-working day
validateDisbursementDateIsOnNonWorkingDay(workingDays, allowTransactionsOnNonWorkingDay);
validateDisbursementDateIsOnHoliday(allowTransactionsOnHoliday, holidays);
}
private void validateActivityNotBeforeClientOrGroupTransferDate(final LoanEvent event, final LocalDate activityDate) {
if (this.client != null && this.client.getOfficeJoiningDate() != null) {
final LocalDate clientOfficeJoiningDate = this.client.getOfficeJoiningDate();
@@ -4991,11 +4643,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
String action = null;
String postfix = null;
switch (event) {
case LOAN_CREATED:
errorMessage = "The date on which a loan is submitted cannot be earlier than client's transfer date to this office";
action = "submittal";
postfix = "cannot.be.before.client.transfer.date";
break;
case LOAN_APPROVED:
errorMessage = "The date on which a loan is approved cannot be earlier than client's transfer date to this office";
action = "approval";
@@ -5400,6 +5047,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return currentDisbursementDetails;
}
public void clearDisbursementDetails() {
this.disbursementDetails.clear();
}
public LoanDisbursementDetails getDisbursementDetails(final LocalDate transactionDate, final BigDecimal transactionAmount) {
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
if (!disbursementDetail.isReversed() && disbursementDetail.getDisbursementDate().equals(transactionDate)
@@ -5525,7 +5176,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
}
public void updateIsInterestRecalculationEnabled() {
this.loanRepaymentScheduleDetail.updateIsInterestRecalculationEnabled(isInterestRecalculationEnabledForProduct());
this.loanRepaymentScheduleDetail.setInterestRecalculationEnabled(isInterestRecalculationEnabledForProduct());
}
public LoanInterestRecalculationDetails loanInterestRecalculationDetails() {
@@ -6196,11 +5847,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
// loanCalendar = loanCalendarInstance.getCalendar();
// }
final Integer graceOnPrincipalPayment = this.loanRepaymentScheduleDetail.graceOnPrincipalPayment();
final Integer graceOnInterestPayment = this.loanRepaymentScheduleDetail.graceOnInterestPayment();
final Integer graceOnInterestCharged = this.loanRepaymentScheduleDetail.graceOnInterestCharged();
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.getGraceOnDueDate();
final Integer graceOnArrearsAgeing = this.loanRepaymentScheduleDetail.getGraceOnArrearsAgeing();
final Money inArrearsToleranceMoney = this.loanRepaymentScheduleDetail.getInArrearsTolerance();
final BigDecimal emiAmount = getFixedEmiAmount();
@@ -6258,7 +5909,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
}
public void updateNumberOfRepayments(Integer numberOfRepayments) {
this.loanRepaymentScheduleDetail.updateNumberOfRepayments(numberOfRepayments);
this.loanRepaymentScheduleDetail.setNumberOfRepayments(numberOfRepayments);
}
public void updateRescheduledOnDate(LocalDate rescheduledOnDate) {
@@ -7134,8 +6785,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return rates;
}
public Integer getLoanType() {
return loanType;
public AccountType getLoanType() {
return AccountType.fromInt(loanType);
}
public void setLoanType(Integer loanType) {
@@ -54,7 +54,6 @@ import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
import org.apache.fineract.portfolio.charge.exception.LoanChargeWithoutMandatoryFieldException;
import org.apache.fineract.portfolio.loanaccount.command.LoanChargeCommand;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
@@ -509,10 +508,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom {
return value.compareTo(BigDecimal.ZERO) > 0;
}
public LoanChargeCommand toCommand() {
return new LoanChargeCommand(getId(), this.charge.getId(), this.amount, this.chargeTime, this.chargeCalculation, getDueLocalDate());
}
public LocalDate getDueLocalDate() {
return this.dueDate;
}
@@ -306,10 +306,10 @@ public final class LoanApplicationTerms {
final Money principalMoney = loanProductRelatedDetail.getPrincipal();
//
final Integer graceOnPrincipalPayment = loanProductRelatedDetail.graceOnPrincipalPayment();
final Integer recurringMoratoriumOnPrincipalPeriods = loanProductRelatedDetail.recurringMoratoriumOnPrincipalPeriods();
final Integer graceOnInterestPayment = loanProductRelatedDetail.graceOnInterestPayment();
final Integer graceOnInterestCharged = loanProductRelatedDetail.graceOnInterestCharged();
final Integer graceOnPrincipalPayment = loanProductRelatedDetail.getGraceOnPrincipalPayment();
final Integer recurringMoratoriumOnPrincipalPeriods = loanProductRelatedDetail.getRecurringMoratoriumOnPrincipalPeriods();
final Integer graceOnInterestPayment = loanProductRelatedDetail.getGraceOnInterestPayment();
final Integer graceOnInterestCharged = loanProductRelatedDetail.getGraceOnInterestCharged();
// Interest recalculation settings
final DaysInMonthType daysInMonthType = loanProductRelatedDetail.fetchDaysInMonthType();
@@ -334,7 +334,7 @@ public final class LoanApplicationTerms {
repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment,
recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate,
inArrearsTolerance, multiDisburseLoan, emiAmount, disbursementDatas, maxOutstandingBalance,
loanProductRelatedDetail.getGraceOnDueDate(), daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
loanProductRelatedDetail.getGraceOnArrearsAgeing(), daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
rescheduleStrategyMethod, compoundingMethod, restCalendarInstance, recalculationFrequencyType, compoundingCalendarInstance,
compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf,
loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper,
@@ -0,0 +1,44 @@
/**
* 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.util.Set;
import java.util.stream.Collectors;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.loanaccount.command.LoanChargeCommand;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(config = MapstructMapperConfig.class)
public interface LoanChargeMapper {
@Mapping(target = "id", source = "loanCharge.id")
@Mapping(target = "chargeId", source = "loanCharge.charge.id")
@Mapping(target = "amount", expression = "java(loanCharge.getAmount(currency).getAmount())")
@Mapping(target = "chargeTimeType", source = "loanCharge.chargeTimeType.value")
@Mapping(target = "chargeCalculationType", source = "loanCharge.chargeCalculation.value")
@Mapping(target = "dueDate", source = "loanCharge.dueDate")
LoanChargeCommand map(LoanCharge loanCharge, MonetaryCurrency currency);
default Set<LoanChargeCommand> map(Set<LoanCharge> loanCharge, MonetaryCurrency currency) {
return loanCharge.stream().map(lc -> map(lc, currency)).collect(Collectors.toSet());
}
}
@@ -0,0 +1,39 @@
/**
* 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.util.Set;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(config = MapstructMapperConfig.class)
public interface LoanCollateralManagementMapper {
@Mapping(target = "id", source = "id")
@Mapping(target = "clientCollateralId", source = "clientCollateralManagement.id")
@Mapping(target = "quantity", source = "quantity")
@Mapping(target = "total", ignore = true)
@Mapping(target = "totalCollateral", ignore = true)
LoanCollateralManagementData map(LoanCollateralManagement loanCollateralManagement);
Set<LoanCollateralManagementData> map(Set<LoanCollateralManagement> loanCollateralManagements);
}
@@ -725,8 +725,6 @@ public class LoanProduct extends AbstractPersistableCustom {
isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment,
enableAutoRepaymentForDownPayment, loanScheduleType, loanScheduleProcessingType, fixedLength);
this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings();
this.loanProductMinMaxConstraints = new LoanProductMinMaxConstraints(defaultMinPrincipal, defaultMaxPrincipal,
defaultMinNominalInterestRatePerPeriod, defaultMaxNominalInterestRatePerPeriod, defaultMinNumberOfInstallments,
defaultMaxNumberOfInstallments);
@@ -1298,21 +1296,21 @@ public class LoanProduct extends AbstractPersistableCustom {
this.loanProductRelatedDetail.isEnableDownPayment())) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_DOWN_PAYMENT);
actualChanges.put(LoanProductConstants.ENABLE_DOWN_PAYMENT, newValue);
this.loanProductRelatedDetail.updateEnableDownPayment(newValue);
this.loanProductRelatedDetail.setEnableDownPayment(newValue);
}
if (command.isChangeInBigDecimalParameterNamed(LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT,
this.loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment())) {
BigDecimal newValue = command.bigDecimalValueOfParameterNamed(LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT);
actualChanges.put(LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, newValue);
this.loanProductRelatedDetail.updateDisbursedAmountPercentageForDownPayment(newValue);
this.loanProductRelatedDetail.setDisbursedAmountPercentageForDownPayment(newValue);
}
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT,
this.loanProductRelatedDetail.isEnableAutoRepaymentForDownPayment())) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT);
actualChanges.put(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, newValue);
this.loanProductRelatedDetail.updateEnableAutoRepaymentForDownPayment(newValue);
this.loanProductRelatedDetail.setEnableAutoRepaymentForDownPayment(newValue);
}
if (command.isChangeInIntegerParameterNamed(LoanProductConstants.REPAYMENT_START_DATE_TYPE,
@@ -32,13 +32,13 @@ public interface LoanProductMinimumRepaymentScheduleRelatedDetail {
Money getPrincipal();
Integer graceOnInterestCharged();
Integer getGraceOnInterestCharged();
Integer graceOnInterestPayment();
Integer getGraceOnInterestPayment();
Integer graceOnPrincipalPayment();
Integer getGraceOnPrincipalPayment();
Integer recurringMoratoriumOnPrincipalPeriods();
Integer getRecurringMoratoriumOnPrincipalPeriods();
Money getInArrearsTolerance();
@@ -60,5 +60,5 @@ public interface LoanProductMinimumRepaymentScheduleRelatedDetail {
AmortizationMethod getAmortizationMethod();
Integer getGraceOnDueDate();
Integer getGraceOnArrearsAgeing();
}
@@ -24,14 +24,11 @@ import jakarta.persistence.Embedded;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
@@ -48,6 +45,8 @@ import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
* a {@link Loan}.
*/
@Embeddable
@Getter
@Setter
public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentScheduleRelatedDetail {
@Embedded
@@ -241,82 +240,32 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
return Money.of(this.currency, this.principal);
}
public void setPrincipal(BigDecimal principal) {
this.principal = principal;
}
@Override
public Integer graceOnInterestCharged() {
return this.graceOnInterestCharged;
}
@Override
public Integer graceOnInterestPayment() {
return this.graceOnInterestPayment;
}
@Override
public Integer graceOnPrincipalPayment() {
return this.graceOnPrincipalPayment;
}
@Override
public Integer recurringMoratoriumOnPrincipalPeriods() {
return this.recurringMoratoriumOnPrincipalPeriods;
}
@Override
public Money getInArrearsTolerance() {
return Money.of(this.currency, this.inArrearsTolerance);
}
// TODO: REVIEW
@Override
public BigDecimal getNominalInterestRatePerPeriod() {
return this.nominalInterestRatePerPeriod == null ? null
: BigDecimal.valueOf(Double.parseDouble(this.nominalInterestRatePerPeriod.stripTrailingZeros().toString()));
}
// TODO: REVIEW
@Override
public PeriodFrequencyType getInterestPeriodFrequencyType() {
return this.interestPeriodFrequencyType == null ? PeriodFrequencyType.INVALID : this.interestPeriodFrequencyType;
}
// TODO: REVIEW
@Override
public BigDecimal getAnnualNominalInterestRate() {
return this.annualNominalInterestRate == null ? null
: BigDecimal.valueOf(Double.parseDouble(this.annualNominalInterestRate.stripTrailingZeros().toString()));
}
@Override
public InterestMethod getInterestMethod() {
return this.interestMethod;
}
@Override
public InterestCalculationPeriodMethod getInterestCalculationPeriodMethod() {
return this.interestCalculationPeriodMethod;
}
@Override
public Integer getRepayEvery() {
return this.repayEvery;
}
@Override
public PeriodFrequencyType getRepaymentPeriodFrequencyType() {
return this.repaymentPeriodFrequencyType;
}
@Override
public Integer getNumberOfRepayments() {
return this.numberOfRepayments;
}
@Override
public AmortizationMethod getAmortizationMethod() {
return this.amortizationMethod;
}
// TODO: Move into assembler
public Map<String, Object> update(final JsonCommand command, final AprCalculator aprCalculator) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(20);
@@ -367,18 +316,6 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
loanScheduleProcessingType = newLoanScheduleProcessingType;
}
final Map<String, Object> loanApplicationAttributeChanges = updateLoanApplicationAttributes(command, aprCalculator);
actualChanges.putAll(loanApplicationAttributeChanges);
return actualChanges;
}
public Map<String, Object> updateLoanApplicationAttributes(final JsonCommand command, final AprCalculator aprCalculator) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(20);
final String localeAsInput = command.locale();
final String principalParamName = "principal";
if (command.isChangeInBigDecimalParameterNamed(principalParamName, this.principal)) {
final BigDecimal newValue = command.bigDecimalValueOfParameterNamed(principalParamName);
@@ -557,63 +494,10 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
this.isEqualAmortization = newValue;
}
validateRepaymentPeriodWithGraceSettings();
return actualChanges;
}
public void updateCurrency(final MonetaryCurrency currency) {
this.currency = currency;
}
public void validateRepaymentPeriodWithGraceSettings() {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanproduct");
if (this.numberOfRepayments <= defaultToZeroIfNull(this.graceOnPrincipalPayment)) {
baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(this.graceOnPrincipalPayment)
.failWithCode(".mustBeLessThan.numberOfRepayments");
}
if (this.numberOfRepayments <= defaultToZeroIfNull(this.graceOnInterestPayment)) {
baseDataValidator.reset().parameter("graceOnInterestPayment").value(this.graceOnInterestPayment)
.failWithCode(".mustBeLessThan.numberOfRepayments");
}
if (this.numberOfRepayments < defaultToZeroIfNull(this.graceOnInterestCharged)) {
baseDataValidator.reset().parameter("graceOnInterestCharged").value(this.graceOnInterestCharged)
.failWithCode(".mustBeLessThan.numberOfRepayments");
}
int graceOnPrincipal = 0;
if (this.getGraceOnPrincipalPayment() != null) {
graceOnPrincipal = this.getGraceOnPrincipalPayment();
}
int recurMoratoriumOnPrincipal = 0;
if (this.recurringMoratoriumOnPrincipalPeriods() != null) {
recurMoratoriumOnPrincipal = this.recurringMoratoriumOnPrincipalPeriods();
}
if ((recurMoratoriumOnPrincipal > 0) && ((this.numberOfRepayments - graceOnPrincipal) % (recurMoratoriumOnPrincipal + 1) != 1)) {
baseDataValidator.reset().parameter("graceOnPrincipalPayments.and.recurringMoratoriumOnPrincipalPeriods")
.value(graceOnPrincipal).value(recurMoratoriumOnPrincipal)
.failWithCode("causes.principal.moratorium.for.last.installment");
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
private Integer defaultToZeroIfNull(final Integer value) {
Integer result = value;
if (value == null) {
result = 0;
}
return result;
}
private void updateInterestRateDerivedFields(final AprCalculator aprCalculator) {
public void updateInterestRateDerivedFields(final AprCalculator aprCalculator) {
this.annualNominalInterestRate = aprCalculator.calculateFrom(this.interestPeriodFrequencyType, this.nominalInterestRatePerPeriod,
this.numberOfRepayments, this.repayEvery, this.repaymentPeriodFrequencyType);
@@ -623,15 +507,6 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
return this.currency.getCode().equalsIgnoreCase(currencyCode);
}
public void updateInterestPeriodFrequencyType(final PeriodFrequencyType interestPeriodFrequencyType) {
this.interestPeriodFrequencyType = interestPeriodFrequencyType;
}
@Override
public Integer getGraceOnDueDate() {
return this.graceOnArrearsAgeing;
}
public DaysInMonthType fetchDaysInMonthType() {
return DaysInMonthType.fromInt(this.daysInMonthType);
}
@@ -640,129 +515,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
return DaysInYearType.fromInt(this.daysInYearType);
}
public boolean isInterestRecalculationEnabled() {
return this.isInterestRecalculationEnabled;
}
public void updateIsInterestRecalculationEnabled(final boolean isInterestRecalculationEnabled) {
this.isInterestRecalculationEnabled = isInterestRecalculationEnabled;
}
public void updateNumberOfRepayments(Integer numberOfRepayments) {
this.numberOfRepayments = numberOfRepayments;
}
public Integer getGraceOnPrincipalPayment() {
return graceOnPrincipalPayment;
}
public void setGraceOnPrincipalPayment(Integer graceOnPrincipalPayment) {
this.graceOnPrincipalPayment = graceOnPrincipalPayment;
}
public Integer getGraceOnInterestPayment() {
return graceOnInterestPayment;
}
public void setGraceOnInterestPayment(Integer graceOnInterestPayment) {
this.graceOnInterestPayment = graceOnInterestPayment;
}
public Integer getGraceOnArrearsAgeing() {
return graceOnArrearsAgeing;
}
public void setGraceOnArrearsAgeing(Integer graceOnArrearsAgeing) {
this.graceOnArrearsAgeing = graceOnArrearsAgeing;
}
public void setInterestMethod(InterestMethod interestMethod) {
this.interestMethod = interestMethod;
}
public void setInterestCalculationPeriodMethod(InterestCalculationPeriodMethod interestCalculationPeriodMethod) {
this.interestCalculationPeriodMethod = interestCalculationPeriodMethod;
}
public void setRepayEvery(Integer repayEvery) {
this.repayEvery = repayEvery;
}
public void setRepaymentPeriodFrequencyType(PeriodFrequencyType repaymentPeriodFrequencyType) {
this.repaymentPeriodFrequencyType = repaymentPeriodFrequencyType;
}
public void setAmortizationMethod(AmortizationMethod amortizationMethod) {
this.amortizationMethod = amortizationMethod;
}
public void setInArrearsTolerance(BigDecimal inArrearsTolerance) {
this.inArrearsTolerance = inArrearsTolerance;
}
public BigDecimal getArrearsTolerance() {
return this.inArrearsTolerance;
}
public void updateForFloatingInterestRates() {
this.nominalInterestRatePerPeriod = null;
this.interestPeriodFrequencyType = PeriodFrequencyType.INVALID;
this.annualNominalInterestRate = null;
}
public boolean isAllowPartialPeriodInterestCalcualtion() {
return this.allowPartialPeriodInterestCalcualtion;
}
public boolean isEqualAmortization() {
return isEqualAmortization;
}
public void setEqualAmortization(boolean isEqualAmortization) {
this.isEqualAmortization = isEqualAmortization;
}
public void setNominalInterestRatePerPeriod(BigDecimal nominalInterestRatePerPeriod) {
this.nominalInterestRatePerPeriod = nominalInterestRatePerPeriod;
}
public boolean isEnableDownPayment() {
return enableDownPayment;
}
public BigDecimal getDisbursedAmountPercentageForDownPayment() {
return disbursedAmountPercentageForDownPayment;
}
public void updateEnableDownPayment(boolean enableDownPayment) {
this.enableDownPayment = enableDownPayment;
}
public void updateDisbursedAmountPercentageForDownPayment(BigDecimal disbursedAmountPercentageForDownPayment) {
this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment;
}
public Boolean isEnableAutoRepaymentForDownPayment() {
return enableAutoRepaymentForDownPayment;
}
public void updateEnableAutoRepaymentForDownPayment(boolean enableAutoRepaymentForDownPayment) {
this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment;
}
public LoanScheduleType getLoanScheduleType() {
return loanScheduleType;
}
public LoanScheduleProcessingType getLoanScheduleProcessingType() {
return loanScheduleProcessingType;
}
public Integer getFixedLength() {
return this.fixedLength;
}
public void setFixedLength(Integer fixedLength) {
this.fixedLength = fixedLength;
}
}