FINERACT-2311: Add buydown fees to loan
This commit is contained in:
+9
@@ -3858,4 +3858,13 @@ public class CommandWrapperBuilder {
|
|||||||
this.href = "/loans/" + loanId;
|
this.href = "/loans/" + loanId;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder makeLoanBuyDownFee(final Long loanId) {
|
||||||
|
this.actionName = "BUYDOWNFEE";
|
||||||
|
this.entityName = "LOAN";
|
||||||
|
this.entityId = null;
|
||||||
|
this.loanId = loanId;
|
||||||
|
this.href = "/loans/" + loanId + "/transactions/template?command=buyDownFee";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-4
@@ -173,6 +173,9 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
|
|||||||
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper
|
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper
|
||||||
.extractLongNamed(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), element);
|
.extractLongNamed(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), element);
|
||||||
|
|
||||||
|
final Long incomeFromBuyDownFeesAccountId = this.fromApiJsonHelper
|
||||||
|
.extractLongNamed(LoanProductAccountingParams.INCOME_FROM_BUY_DOWN.getValue(), element);
|
||||||
|
|
||||||
final Long writeOffAccountId = this.fromApiJsonHelper.extractLongNamed(LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(),
|
final Long writeOffAccountId = this.fromApiJsonHelper.extractLongNamed(LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(),
|
||||||
element);
|
element);
|
||||||
final Long overPaymentAccountId = this.fromApiJsonHelper.extractLongNamed(LoanProductAccountingParams.OVERPAYMENT.getValue(),
|
final Long overPaymentAccountId = this.fromApiJsonHelper.extractLongNamed(LoanProductAccountingParams.OVERPAYMENT.getValue(),
|
||||||
@@ -198,12 +201,14 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
|
|||||||
case ACCRUAL_PERIODIC:
|
case ACCRUAL_PERIODIC:
|
||||||
populateChangesForAccrualBasedAccounting(changes, fundAccountId, loanPortfolioAccountId, incomeFromInterestId,
|
populateChangesForAccrualBasedAccounting(changes, fundAccountId, loanPortfolioAccountId, incomeFromInterestId,
|
||||||
incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId,
|
incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId,
|
||||||
incomeFromRecoveryAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId);
|
incomeFromRecoveryAccountId, incomeFromBuyDownFeesAccountId, receivableInterestAccountId, receivableFeeAccountId,
|
||||||
|
receivablePenaltyAccountId);
|
||||||
break;
|
break;
|
||||||
case ACCRUAL_UPFRONT:
|
case ACCRUAL_UPFRONT:
|
||||||
populateChangesForAccrualBasedAccounting(changes, fundAccountId, loanPortfolioAccountId, incomeFromInterestId,
|
populateChangesForAccrualBasedAccounting(changes, fundAccountId, loanPortfolioAccountId, incomeFromInterestId,
|
||||||
incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId,
|
incomeFromFeeId, incomeFromPenaltyId, writeOffAccountId, overPaymentAccountId, transfersInSuspenseAccountId,
|
||||||
incomeFromRecoveryAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId);
|
incomeFromRecoveryAccountId, incomeFromBuyDownFeesAccountId, receivableInterestAccountId, receivableFeeAccountId,
|
||||||
|
receivablePenaltyAccountId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,8 +218,8 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
|
|||||||
private void populateChangesForAccrualBasedAccounting(final Map<String, Object> changes, final Long fundAccountId,
|
private void populateChangesForAccrualBasedAccounting(final Map<String, Object> changes, final Long fundAccountId,
|
||||||
final Long loanPortfolioAccountId, final Long incomeFromInterestId, final Long incomeFromFeeId, final Long incomeFromPenaltyId,
|
final Long loanPortfolioAccountId, final Long incomeFromInterestId, final Long incomeFromFeeId, final Long incomeFromPenaltyId,
|
||||||
final Long writeOffAccountId, final Long overPaymentAccountId, final Long transfersInSuspenseAccountId,
|
final Long writeOffAccountId, final Long overPaymentAccountId, final Long transfersInSuspenseAccountId,
|
||||||
final Long incomeFromRecoveryAccountId, final Long receivableInterestAccountId, final Long receivableFeeAccountId,
|
final Long incomeFromRecoveryAccountId, final Long incomeFromBuyDownFeesAccountId, final Long receivableInterestAccountId,
|
||||||
final Long receivablePenaltyAccountId) {
|
final Long receivableFeeAccountId, final Long receivablePenaltyAccountId) {
|
||||||
|
|
||||||
changes.put(LoanProductAccountingParams.INTEREST_RECEIVABLE.getValue(), receivableInterestAccountId);
|
changes.put(LoanProductAccountingParams.INTEREST_RECEIVABLE.getValue(), receivableInterestAccountId);
|
||||||
changes.put(LoanProductAccountingParams.FEES_RECEIVABLE.getValue(), receivableFeeAccountId);
|
changes.put(LoanProductAccountingParams.FEES_RECEIVABLE.getValue(), receivableFeeAccountId);
|
||||||
|
|||||||
+2
@@ -181,6 +181,8 @@ public interface LoanApiConstants {
|
|||||||
String CAPITALIZED_INCOME_ADJUSTMENT_TRANSACTION_COMMAND = "capitalizedIncomeAdjustment";
|
String CAPITALIZED_INCOME_ADJUSTMENT_TRANSACTION_COMMAND = "capitalizedIncomeAdjustment";
|
||||||
String CONTRACT_TERMINATION_COMMAND = "contractTermination";
|
String CONTRACT_TERMINATION_COMMAND = "contractTermination";
|
||||||
String UNDO_CONTRACT_TERMINATION_COMMAND = "undoContractTermination";
|
String UNDO_CONTRACT_TERMINATION_COMMAND = "undoContractTermination";
|
||||||
|
String BUY_DOWN_FEE_COMMAND = "buyDownFee";
|
||||||
|
String BUY_DOWN_FEE_ADJUSTMENT_COMMAND = "buyDownFeeAdjustment";
|
||||||
|
|
||||||
// Data Validator names
|
// Data Validator names
|
||||||
String LOAN_FRAUD_DATAVALIDATOR_PREFIX = "loans.fraud";
|
String LOAN_FRAUD_DATAVALIDATOR_PREFIX = "loans.fraud";
|
||||||
|
|||||||
+4
@@ -72,6 +72,8 @@ public class LoanTransactionEnumData implements Serializable {
|
|||||||
private final boolean capitalizedIncomeAdjustment;
|
private final boolean capitalizedIncomeAdjustment;
|
||||||
private final boolean capitalizedIncomeAmortizationAdjustment;
|
private final boolean capitalizedIncomeAmortizationAdjustment;
|
||||||
private final boolean contractTermination;
|
private final boolean contractTermination;
|
||||||
|
private final boolean buyDownFee;
|
||||||
|
private final boolean buyDownFeeAdjustment;
|
||||||
|
|
||||||
public LoanTransactionEnumData(final Long id, final String code, final String value) {
|
public LoanTransactionEnumData(final Long id, final String code, final String value) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -114,6 +116,8 @@ public class LoanTransactionEnumData implements Serializable {
|
|||||||
this.capitalizedIncomeAmortizationAdjustment = Long
|
this.capitalizedIncomeAmortizationAdjustment = Long
|
||||||
.valueOf(LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT.getValue()).equals(this.id);
|
.valueOf(LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT.getValue()).equals(this.id);
|
||||||
this.contractTermination = Long.valueOf(LoanTransactionType.CONTRACT_TERMINATION.getValue()).equals(this.id);
|
this.contractTermination = Long.valueOf(LoanTransactionType.CONTRACT_TERMINATION.getValue()).equals(this.id);
|
||||||
|
this.buyDownFee = Long.valueOf(LoanTransactionType.BUY_DOWN_FEE.getValue()).equals(this.id);
|
||||||
|
this.buyDownFeeAdjustment = Long.valueOf(LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getValue()).equals(this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRepaymentType() {
|
public boolean isRepaymentType() {
|
||||||
|
|||||||
+9
@@ -961,4 +961,13 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
this.dateOf = transactionDate;
|
this.dateOf = transactionDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static LoanTransaction buyDownFee(final Loan loan, final Money amount, final PaymentDetail paymentDetail,
|
||||||
|
final LocalDate transactionDate, final ExternalId externalId) {
|
||||||
|
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.BUY_DOWN_FEE, paymentDetail, amount.getAmount(),
|
||||||
|
transactionDate, externalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBuyDownFee() {
|
||||||
|
return LoanTransactionType.BUY_DOWN_FEE.equals(this.typeOf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -73,6 +73,9 @@ public enum LoanTransactionType {
|
|||||||
CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT(39, "loanTransactionType.capitalizedIncomeAmortizationAdjustment"), //
|
CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT(39, "loanTransactionType.capitalizedIncomeAmortizationAdjustment"), //
|
||||||
// Kind of Final Transactions
|
// Kind of Final Transactions
|
||||||
CONTRACT_TERMINATION(38, "loanTransactionType.contractTermination"), //
|
CONTRACT_TERMINATION(38, "loanTransactionType.contractTermination"), //
|
||||||
|
|
||||||
|
BUY_DOWN_FEE(40, "loanTransactionType.buyDownFee"), //
|
||||||
|
BUY_DOWN_FEE_ADJUSTMENT(41, "loanTransactionType.buyDownFeeAdjustment"), //
|
||||||
;
|
;
|
||||||
|
|
||||||
private final Integer value;
|
private final Integer value;
|
||||||
@@ -128,6 +131,8 @@ public enum LoanTransactionType {
|
|||||||
case 37 -> LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT;
|
case 37 -> LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT;
|
||||||
case 38 -> LoanTransactionType.CONTRACT_TERMINATION;
|
case 38 -> LoanTransactionType.CONTRACT_TERMINATION;
|
||||||
case 39 -> LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT;
|
case 39 -> LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT;
|
||||||
|
case 40 -> LoanTransactionType.BUY_DOWN_FEE;
|
||||||
|
case 41 -> LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT;
|
||||||
default -> LoanTransactionType.INVALID;
|
default -> LoanTransactionType.INVALID;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -256,4 +261,12 @@ public enum LoanTransactionType {
|
|||||||
public boolean isContractTermination() {
|
public boolean isContractTermination() {
|
||||||
return this == LoanTransactionType.CONTRACT_TERMINATION;
|
return this == LoanTransactionType.CONTRACT_TERMINATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isBuyDownFee() {
|
||||||
|
return this == LoanTransactionType.BUY_DOWN_FEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBuyDownFeeAdjustment() {
|
||||||
|
return this == LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 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.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AbstractPlatformDomainRuleException} thrown when loan transaction processing violates a domain rule.
|
||||||
|
*/
|
||||||
|
public class LoanTransactionProcessingException extends AbstractPlatformDomainRuleException {
|
||||||
|
|
||||||
|
public LoanTransactionProcessingException(final String defaultUserMessage, final Object... defaultUserMessageArgs) {
|
||||||
|
super("error.msg.loan.transaction.processing", defaultUserMessage, defaultUserMessageArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoanTransactionProcessingException(final String action, final String defaultUserMessage,
|
||||||
|
final Object... defaultUserMessageArgs) {
|
||||||
|
super("error.msg.loan.transaction." + action, defaultUserMessage, defaultUserMessageArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
@@ -338,6 +338,10 @@ public final class LoanEnumerations {
|
|||||||
case CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT -> new LoanTransactionEnumData(
|
case CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT -> new LoanTransactionEnumData(
|
||||||
LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT.getValue().longValue(),
|
LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT.getValue().longValue(),
|
||||||
LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT.getCode(), "Capitalized Income Amortization Adjustment");
|
LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT.getCode(), "Capitalized Income Amortization Adjustment");
|
||||||
|
case BUY_DOWN_FEE -> new LoanTransactionEnumData(LoanTransactionType.BUY_DOWN_FEE.getValue().longValue(),
|
||||||
|
LoanTransactionType.BUY_DOWN_FEE.getCode(), "Buy Down Fee");
|
||||||
|
case BUY_DOWN_FEE_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getValue().longValue(),
|
||||||
|
LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT.getCode(), "Buy Down Fee Adjustment");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.BuyDownFeePlatformService;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@CommandType(entity = "LOAN", action = "BUYDOWNFEE")
|
||||||
|
public class AddBuyDownFeeCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final BuyDownFeePlatformService buyDownFeePlatformService;
|
||||||
|
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this.buyDownFeePlatformService.makeLoanBuyDownFee(command.getLoanId(), command);
|
||||||
|
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||||
|
dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.buy.down.fee",
|
||||||
|
"Buy Down Fee");
|
||||||
|
return CommandProcessingResult.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
public interface BuyDownFeePlatformService {
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
CommandProcessingResult makeLoanBuyDownFee(Long loanId, JsonCommand command);
|
||||||
|
}
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
|
||||||
|
import org.apache.fineract.portfolio.group.domain.Group;
|
||||||
|
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||||
|
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
|
||||||
|
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||||
|
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformService {
|
||||||
|
|
||||||
|
private final ProgressiveLoanTransactionValidator loanTransactionValidator;
|
||||||
|
private final LoanAssembler loanAssembler;
|
||||||
|
private final LoanTransactionRepository loanTransactionRepository;
|
||||||
|
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
|
||||||
|
private final LoanJournalEntryPoster loanJournalEntryPoster;
|
||||||
|
private final NoteWritePlatformService noteWritePlatformService;
|
||||||
|
private final ExternalIdFactory externalIdFactory;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult makeLoanBuyDownFee(final Long loanId, final JsonCommand command) {
|
||||||
|
|
||||||
|
this.loanTransactionValidator.validateBuyDownFee(command, loanId);
|
||||||
|
|
||||||
|
final Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||||
|
checkClientOrGroupActive(loan);
|
||||||
|
|
||||||
|
final List<Long> existingTransactionIds = new ArrayList<>(loanTransactionRepository.findTransactionIdsByLoan(loan));
|
||||||
|
final List<Long> existingReversedTransactionIds = new ArrayList<>(loanTransactionRepository.findReversedTransactionIdsByLoan(loan));
|
||||||
|
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// Create payment details
|
||||||
|
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
||||||
|
|
||||||
|
// Extract transaction details
|
||||||
|
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||||
|
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||||
|
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, "externalId");
|
||||||
|
|
||||||
|
// Create buy down fee transaction
|
||||||
|
final Money buyDownFeeAmount = Money.of(loan.getCurrency(), transactionAmount); // FLAT calculation
|
||||||
|
final LoanTransaction buyDownFeeTransaction = LoanTransaction.buyDownFee(loan, buyDownFeeAmount, paymentDetail, transactionDate,
|
||||||
|
txnExternalId);
|
||||||
|
|
||||||
|
// Add to loan (NO schedule recalculation as per requirements)
|
||||||
|
loan.addLoanTransaction(buyDownFeeTransaction);
|
||||||
|
|
||||||
|
// Save transaction
|
||||||
|
loanTransactionRepository.saveAndFlush(buyDownFeeTransaction);
|
||||||
|
|
||||||
|
// Update loan derived fields
|
||||||
|
loan.updateLoanScheduleDependentDerivedFields();
|
||||||
|
|
||||||
|
// Add note if provided
|
||||||
|
final String noteText = command.stringValueOfParameterNamed("note");
|
||||||
|
if (StringUtils.isNotBlank(noteText)) {
|
||||||
|
noteWritePlatformService.createLoanTransactionNote(buyDownFeeTransaction.getId(), noteText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post journal entries
|
||||||
|
loanJournalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder() //
|
||||||
|
.withClientId(loan.getClientId()) //
|
||||||
|
.withOfficeId(loan.getOfficeId()) //
|
||||||
|
.withLoanId(loan.getId()) //
|
||||||
|
.withEntityId(buyDownFeeTransaction.getId()) //
|
||||||
|
.withEntityExternalId(buyDownFeeTransaction.getExternalId()) //
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkClientOrGroupActive(final Loan loan) {
|
||||||
|
final Client client = loan.client();
|
||||||
|
if (client != null && client.isNotActive()) {
|
||||||
|
throw new ClientNotActiveException(client.getId());
|
||||||
|
}
|
||||||
|
final Group group = loan.group();
|
||||||
|
if (group != null && group.isNotActive()) {
|
||||||
|
throw new GroupNotActiveException(group.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -28,4 +28,6 @@ public interface ProgressiveLoanTransactionValidator extends LoanTransactionVali
|
|||||||
void validateCapitalizedIncomeAdjustment(JsonCommand command, Long loanId, Long capitalizedIncomeTransactionId);
|
void validateCapitalizedIncomeAdjustment(JsonCommand command, Long loanId, Long capitalizedIncomeTransactionId);
|
||||||
|
|
||||||
void validateContractTerminationUndo(JsonCommand command, Long loanId);
|
void validateContractTerminationUndo(JsonCommand command, Long loanId);
|
||||||
|
|
||||||
|
void validateBuyDownFee(JsonCommand command, Long loanId);
|
||||||
}
|
}
|
||||||
|
|||||||
+58
@@ -23,6 +23,7 @@ import com.google.gson.reflect.TypeToken;
|
|||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -33,9 +34,11 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
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.data.DataValidatorBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
@@ -53,6 +56,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionProcessingException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||||
@@ -269,6 +273,60 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final List<String> BUY_DOWN_FEE_TRANSACTION_SUPPORTED_PARAMETERS = List
|
||||||
|
.of(new String[] { "transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId" });
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validateBuyDownFee(JsonCommand command, Long loanId) {
|
||||||
|
final String json = command.json();
|
||||||
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, BUY_DOWN_FEE_TRANSACTION_SUPPORTED_PARAMETERS);
|
||||||
|
|
||||||
|
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||||
|
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
|
||||||
|
.resource("loan.transaction.buyDownFee");
|
||||||
|
|
||||||
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
|
||||||
|
|
||||||
|
if (!loan.getLoanProductRelatedDetail().isEnableBuyDownFee()) {
|
||||||
|
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("buy.down.fee.not.enabled",
|
||||||
|
"Buy down fee is not enabled for this loan product");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
validateBuyDownFeeEligibility(loan);
|
||||||
|
|
||||||
|
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
|
||||||
|
baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
|
||||||
|
|
||||||
|
// Validate transaction date is on or after first disbursement
|
||||||
|
if (transactionDate != null) {
|
||||||
|
final LocalDate firstDisbursementDate = loan.getDisbursementDate();
|
||||||
|
if (firstDisbursementDate != null && transactionDate.isBefore(firstDisbursementDate)) {
|
||||||
|
baseDataValidator.reset().parameter("transactionDate").failWithCode("cannot.be.before.first.disbursement.date");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
|
||||||
|
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
|
||||||
|
|
||||||
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateBuyDownFeeEligibility(Loan loan) {
|
||||||
|
if (!loan.getStatus().isActive()) {
|
||||||
|
throw new LoanTransactionProcessingException("Buy Down fees can only be added to active loans");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||||
|
if (!dataValidationErrors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||||
|
dataValidationErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Delegates
|
// Delegates
|
||||||
@Override
|
@Override
|
||||||
public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId) {
|
public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId) {
|
||||||
|
|||||||
+24
@@ -145,6 +145,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
|||||||
if (transactionType.isCapitalizedIncomeAmortizationAdjustment()) {
|
if (transactionType.isCapitalizedIncomeAmortizationAdjustment()) {
|
||||||
createJournalEntriesForCapitalizedIncomeAmortizationAdjustment(loanDTO, loanTransactionDTO, office);
|
createJournalEntriesForCapitalizedIncomeAmortizationAdjustment(loanDTO, loanTransactionDTO, office);
|
||||||
}
|
}
|
||||||
|
// Handle Buy Down Fee
|
||||||
|
if (transactionType.isBuyDownFee()) {
|
||||||
|
createJournalEntriesForBuyDownFee(loanDTO, loanTransactionDTO, office);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,6 +416,26 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createJournalEntriesForBuyDownFee(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
|
||||||
|
final Office office) {
|
||||||
|
// loan properties
|
||||||
|
final Long loanProductId = loanDTO.getLoanProductId();
|
||||||
|
final Long loanId = loanDTO.getLoanId();
|
||||||
|
final String currencyCode = loanDTO.getCurrencyCode();
|
||||||
|
|
||||||
|
// transaction properties
|
||||||
|
final String transactionId = loanTransactionDTO.getTransactionId();
|
||||||
|
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
|
||||||
|
final BigDecimal amount = loanTransactionDTO.getAmount();
|
||||||
|
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
|
||||||
|
|
||||||
|
if (MathUtil.isGreaterThanZero(amount)) {
|
||||||
|
this.helper.createJournalEntriesForLoan(office, currencyCode, AccrualAccountsForLoan.BUY_DOWN_EXPENSE.getValue(),
|
||||||
|
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), loanProductId, paymentTypeId, loanId, transactionId,
|
||||||
|
transactionDate, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void createJournalEntriesForInterestPaymentWaiverOrInterestRefund(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO,
|
private void createJournalEntriesForInterestPaymentWaiverOrInterestRefund(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO,
|
||||||
Office office) {
|
Office office) {
|
||||||
final Long loanProductId = loanDTO.getLoanProductId();
|
final Long loanProductId = loanDTO.getLoanProductId();
|
||||||
|
|||||||
+4
@@ -596,6 +596,8 @@ public class LoanTransactionsApiResource {
|
|||||||
commandRequest = builder.undoReAmortize(resolvedLoanId).build();
|
commandRequest = builder.undoReAmortize(resolvedLoanId).build();
|
||||||
} else if (CommandParameterUtil.is(commandParam, CAPITALIZED_INCOME)) {
|
} else if (CommandParameterUtil.is(commandParam, CAPITALIZED_INCOME)) {
|
||||||
commandRequest = builder.addCapitalizedIncome(resolvedLoanId).build();
|
commandRequest = builder.addCapitalizedIncome(resolvedLoanId).build();
|
||||||
|
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_COMMAND)) {
|
||||||
|
commandRequest = builder.makeLoanBuyDownFee(resolvedLoanId).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandRequest == null) {
|
if (commandRequest == null) {
|
||||||
@@ -684,6 +686,8 @@ public class LoanTransactionsApiResource {
|
|||||||
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.CAPITALIZED_INCOME_ADJUSTMENT_TRANSACTION_COMMAND)) {
|
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.CAPITALIZED_INCOME_ADJUSTMENT_TRANSACTION_COMMAND)) {
|
||||||
transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId,
|
transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId,
|
||||||
LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT, transactionId);
|
LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT, transactionId);
|
||||||
|
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_COMMAND)) {
|
||||||
|
transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId);
|
||||||
} else {
|
} else {
|
||||||
throw new UnrecognizedQueryParamException("command", commandParam);
|
throw new UnrecognizedQueryParamException("command", commandParam);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -103,6 +103,8 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionVa
|
|||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
|
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformServiceImpl;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.BuyDownFeePlatformService;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.BuyDownFeeWritePlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService;
|
||||||
@@ -149,6 +151,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
|
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
|
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
|
||||||
@@ -158,6 +161,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
|
|||||||
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
|
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
|
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||||
|
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
|
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.rate.service.RateAssembler;
|
import org.apache.fineract.portfolio.rate.service.RateAssembler;
|
||||||
@@ -372,6 +376,16 @@ public class LoanAccountConfiguration {
|
|||||||
workingDaysRepository, loanScheduleFactory, floatingRatesReadPlatformService, calendarReadPlatformService, noteRepository);
|
workingDaysRepository, loanScheduleFactory, floatingRatesReadPlatformService, calendarReadPlatformService, noteRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(BuyDownFeePlatformService.class)
|
||||||
|
public BuyDownFeePlatformService buyDownFeePlatformService(ProgressiveLoanTransactionValidator loanTransactionValidator,
|
||||||
|
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
|
||||||
|
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster loanJournalEntryPoster,
|
||||||
|
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory) {
|
||||||
|
return new BuyDownFeeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
|
||||||
|
paymentDetailWritePlatformService, loanJournalEntryPoster, noteWritePlatformService, externalIdFactory);
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean(LoanWritePlatformService.class)
|
@ConditionalOnMissingBean(LoanWritePlatformService.class)
|
||||||
public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext context,
|
public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext context,
|
||||||
@@ -407,7 +421,7 @@ public class LoanAccountConfiguration {
|
|||||||
LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService,
|
LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService,
|
||||||
LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper loanMapper,
|
LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper loanMapper,
|
||||||
LoanTransactionProcessingService loanTransactionProcessingService, final LoanBalanceService loanBalanceService,
|
LoanTransactionProcessingService loanTransactionProcessingService, final LoanBalanceService loanBalanceService,
|
||||||
LoanTransactionService loanTransactionService) {
|
LoanTransactionService loanTransactionService, BuyDownFeePlatformService buyDownFeePlatformService) {
|
||||||
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
|
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
|
||||||
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
|
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
|
||||||
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
|
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
|
||||||
|
|||||||
+1
-1
@@ -175,7 +175,7 @@ public class LoanProductsApiResource {
|
|||||||
@Operation(summary = "Create a Loan Product", description = "Depending of the Accounting Rule (accountingRule) selected, additional fields with details of the appropriate Ledger Account identifiers would need to be passed in.\n"
|
@Operation(summary = "Create a Loan Product", description = "Depending of the Accounting Rule (accountingRule) selected, additional fields with details of the appropriate Ledger Account identifiers would need to be passed in.\n"
|
||||||
+ "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n"
|
+ "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n"
|
||||||
+ "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n"
|
+ "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n"
|
||||||
+ "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, chargeOffReasonToExpenseAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n"
|
+ "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, chargeOffReasonToExpenseAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType,enableBuyDownFee\n\n"
|
||||||
+ "Additional Mandatory Fields for Cash(2) based accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
|
+ "Additional Mandatory Fields for Cash(2) based accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
|
||||||
+ "Additional Mandatory Fields for periodic (3) and upfront (4)accrual accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
|
+ "Additional Mandatory Fields for periodic (3) and upfront (4)accrual accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
|
||||||
+ "Additional Mandatory Fields if interest recalculation is enabled(true): interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType\n\n"
|
+ "Additional Mandatory Fields if interest recalculation is enabled(true): interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType\n\n"
|
||||||
|
|||||||
+2
-1
@@ -200,7 +200,8 @@ public final class LoanProductDataValidator {
|
|||||||
LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME,
|
LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME,
|
||||||
LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME, LoanProductConstants.CAPITALIZED_INCOME_TYPE_PARAM_NAME,
|
LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME, LoanProductConstants.CAPITALIZED_INCOME_TYPE_PARAM_NAME,
|
||||||
LoanProductConstants.ENABLE_BUY_DOWN_FEE_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_CALCULATION_TYPE_PARAM_NAME,
|
LoanProductConstants.ENABLE_BUY_DOWN_FEE_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_CALCULATION_TYPE_PARAM_NAME,
|
||||||
LoanProductConstants.BUY_DOWN_FEE_STRATEGY_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME));
|
LoanProductConstants.BUY_DOWN_FEE_STRATEGY_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME,
|
||||||
|
LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), LoanProductAccountingParams.INCOME_FROM_BUY_DOWN.getValue()));
|
||||||
|
|
||||||
private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName,
|
private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName,
|
||||||
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName,
|
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName,
|
||||||
|
|||||||
@@ -204,4 +204,5 @@
|
|||||||
<include file="parts/0183_add_LoanCapitalizedIncomeTransactionCreatedBusinessEvent.xml" relativeToChangelogFile="true" />
|
<include file="parts/0183_add_LoanCapitalizedIncomeTransactionCreatedBusinessEvent.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0184_add_document_event_configuration.xml" relativeToChangelogFile="true" />
|
<include file="parts/0184_add_document_event_configuration.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0185_loan_buy_down_fee.xml" relativeToChangelogFile="true" />
|
<include file="parts/0185_loan_buy_down_fee.xml" relativeToChangelogFile="true" />
|
||||||
|
<include file="parts/0186_add_buy_down_fee_to_loan_product.xml" relativeToChangelogFile="true" />
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
<changeSet author="fineract" id="1">
|
||||||
|
<insert tableName="r_enum_value">
|
||||||
|
<column name="enum_name" value="transaction_type_enum"/>
|
||||||
|
<column name="enum_id" valueNumeric="40"/>
|
||||||
|
<column name="enum_message_property" value="Buy Down Fee"/>
|
||||||
|
<column name="enum_value" value="Buy Down Fee"/>
|
||||||
|
<column name="enum_type" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
<changeSet author="fineract" id="2">
|
||||||
|
<insert tableName="r_enum_value">
|
||||||
|
<column name="enum_name" value="transaction_type_enum"/>
|
||||||
|
<column name="enum_id" valueNumeric="41"/>
|
||||||
|
<column name="enum_message_property" value="Buy Down Fee Adjustment"/>
|
||||||
|
<column name="enum_value" value="Buy Down Fee Adjustment"/>
|
||||||
|
<column name="enum_type" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+264
@@ -0,0 +1,264 @@
|
|||||||
|
/**
|
||||||
|
* 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.integrationtests;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
|
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
|
||||||
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansResponse;
|
||||||
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.Utils;
|
||||||
|
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for Buy Down Fee functionality in Progressive Loans
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@ExtendWith({ LoanTestLifecycleExtension.class })
|
||||||
|
public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
|
||||||
|
|
||||||
|
private Long clientId;
|
||||||
|
private Long loanId;
|
||||||
|
|
||||||
|
// Buy Down Fee accounts for accrual-based accounting
|
||||||
|
private final Account buyDownExpenseAccount = accountHelper.createExpenseAccount();
|
||||||
|
private final Account deferredIncomeLiabilityAccount = accountHelper.createLiabilityAccount();
|
||||||
|
|
||||||
|
// Additional receivable accounts required for accrual-based accounting
|
||||||
|
private final Account interestReceivableAccount = accountHelper.createAssetAccount();
|
||||||
|
private final Account feeReceivableAccount = accountHelper.createAssetAccount();
|
||||||
|
private final Account penaltyReceivableAccount = accountHelper.createAssetAccount();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void beforeEach() {
|
||||||
|
runAt("01 September 2024", () -> {
|
||||||
|
clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
|
||||||
|
.createLoanProduct(createProgressiveLoanProductWithBuyDownFee());
|
||||||
|
|
||||||
|
// Apply for the loan with proper progressive loan settings
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId,
|
||||||
|
loanProductsResponse.getResourceId(), "01 September 2024", 1000.0, 10.0, 12, null));
|
||||||
|
loanId = postLoansResponse.getLoanId();
|
||||||
|
loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 September 2024"));
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 September 2024");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuyDownFeeOnProgressiveLoan() {
|
||||||
|
runAt("02 September 2024", () -> {
|
||||||
|
// Verify loan product has buy down fee enabled
|
||||||
|
final GetLoansLoanIdResponse loanDetailsBeforeTransaction = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
assertNotNull(loanDetailsBeforeTransaction);
|
||||||
|
log.info("Loan Product: {}", loanDetailsBeforeTransaction.getLoanProductName());
|
||||||
|
|
||||||
|
// Create buy down fee transaction
|
||||||
|
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 500.0, "02 September 2024");
|
||||||
|
|
||||||
|
assertNotNull(buyDownFeeTransactionId);
|
||||||
|
|
||||||
|
// Verify transaction was created in loan details
|
||||||
|
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
assertNotNull(loanDetails);
|
||||||
|
|
||||||
|
// Find the buy down fee transaction
|
||||||
|
boolean buyDownFeeFound = false;
|
||||||
|
for (GetLoansLoanIdTransactions transaction : loanDetails.getTransactions()) {
|
||||||
|
if (transaction.getType() != null && transaction.getType().getId() != null && transaction.getType().getId().equals(40L)) {
|
||||||
|
buyDownFeeFound = true;
|
||||||
|
assertEquals(0, BigDecimal.valueOf(500.0).compareTo(transaction.getAmount()));
|
||||||
|
assertEquals(Long.valueOf(40), transaction.getType().getId());
|
||||||
|
assertEquals("Buy Down Fee", transaction.getType().getValue());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(buyDownFeeFound, "Buy down fee transaction should be found in loan transactions");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuyDownFeeWithNote() {
|
||||||
|
runAt("03 September 2024", () -> {
|
||||||
|
String externalId = UUID.randomUUID().toString();
|
||||||
|
String noteText = "Buy Down Fee - Test Note";
|
||||||
|
|
||||||
|
PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeLoanBuyDownFee(loanId,
|
||||||
|
new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("03 September 2024").locale("en")
|
||||||
|
.transactionAmount(250.0).externalId(externalId).note(noteText));
|
||||||
|
|
||||||
|
assertNotNull(response.getResourceId());
|
||||||
|
|
||||||
|
// Verify transaction details
|
||||||
|
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
GetLoansLoanIdTransactions buyDownFeeTransaction = loanDetails.getTransactions().stream()
|
||||||
|
.filter(t -> t.getType() != null && t.getType().getId() != null && t.getType().getId().equals(40L))
|
||||||
|
.filter(t -> externalId.equals(t.getExternalId())).findFirst().orElse(null);
|
||||||
|
|
||||||
|
assertNotNull(buyDownFeeTransaction, "Buy down fee transaction should exist");
|
||||||
|
assertEquals(0, BigDecimal.valueOf(250.0).compareTo(buyDownFeeTransaction.getAmount()));
|
||||||
|
assertEquals(externalId, buyDownFeeTransaction.getExternalId());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultipleBuyDownFees() {
|
||||||
|
runAt("04 September 2024", () -> {
|
||||||
|
// Add first buy down fee
|
||||||
|
Long firstBuyDownFeeId = addBuyDownFeeForLoan(loanId, 200.0, "04 September 2024");
|
||||||
|
|
||||||
|
// Add second buy down fee
|
||||||
|
Long secondBuyDownFeeId = addBuyDownFeeForLoan(loanId, 150.0, "04 September 2024");
|
||||||
|
|
||||||
|
assertNotNull(firstBuyDownFeeId);
|
||||||
|
assertNotNull(secondBuyDownFeeId);
|
||||||
|
|
||||||
|
// Verify both transactions exist
|
||||||
|
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
long buyDownFeeCount = loanDetails.getTransactions().stream()
|
||||||
|
.filter(t -> t.getType() != null && t.getType().getId() != null && t.getType().getId().equals(40L)).count();
|
||||||
|
|
||||||
|
assertEquals(2, buyDownFeeCount, "Should have 2 buy down fee transactions");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuyDownFeeAccountingEntries() {
|
||||||
|
runAt("04 September 2024", () -> {
|
||||||
|
// Add Buy Down fee transaction
|
||||||
|
Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 250.0, "04 September 2024");
|
||||||
|
assertNotNull(buyDownFeeTransactionId);
|
||||||
|
|
||||||
|
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
GetLoansLoanIdTransactions buyDownFeeTransaction = loanDetails.getTransactions().stream()
|
||||||
|
.filter(t -> t.getType() != null && t.getType().getId() != null && t.getType().getId().equals(40L))
|
||||||
|
.filter(t -> buyDownFeeTransactionId.equals(t.getId())).findFirst().orElse(null);
|
||||||
|
|
||||||
|
assertNotNull(buyDownFeeTransaction, "Buy down fee transaction should exist");
|
||||||
|
assertEquals(0, BigDecimal.valueOf(250.0).compareTo(buyDownFeeTransaction.getAmount()));
|
||||||
|
|
||||||
|
verifyTRJournalEntries(buyDownFeeTransactionId, debit(buyDownExpenseAccount, 250.0), // DR: Buy Down Expense
|
||||||
|
credit(deferredIncomeLiabilityAccount, 250.0) // CR: Deferred Income Liability
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info("Buy Down Fee transaction created successfully (accounting validation pending client model regeneration)");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuyDownFeeValidation() {
|
||||||
|
runAt("05 September 2024", () -> {
|
||||||
|
// Test with negative amount (should fail)
|
||||||
|
try {
|
||||||
|
PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeLoanBuyDownFee(loanId,
|
||||||
|
new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("05 September 2024")
|
||||||
|
.locale("en").transactionAmount(-100.0).note("Invalid negative amount"));
|
||||||
|
assertTrue(false, "Buy down fee with negative amount should have failed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Expected: validation should prevent negative amounts
|
||||||
|
log.info("Expected validation error for negative amount: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with zero amount (should fail)
|
||||||
|
try {
|
||||||
|
PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeLoanBuyDownFee(loanId,
|
||||||
|
new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("05 September 2024")
|
||||||
|
.locale("en").transactionAmount(0.0).note("Invalid zero amount"));
|
||||||
|
assertTrue(false, "Buy down fee with zero amount should have failed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Expected: validation should prevent zero amounts
|
||||||
|
log.info("Expected validation error for zero amount: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a progressive loan product with buy down fee enabled
|
||||||
|
*/
|
||||||
|
private PostLoanProductsRequest createProgressiveLoanProductWithBuyDownFee() {
|
||||||
|
// Create a progressive loan product with accrual-based accounting and proper GL mappings
|
||||||
|
return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("BUY_DOWN_FEE_PROGRESSIVE_", 6))
|
||||||
|
.shortName(Utils.uniqueRandomStringGenerator("", 4)).description("Progressive loan product with buy down fee enabled")
|
||||||
|
.includeInBorrowerCycle(false).useBorrowerCycle(false).currencyCode("USD").digitsAfterDecimal(2).principal(1000.0)
|
||||||
|
.minPrincipal(100.0).maxPrincipal(10000.0).numberOfRepayments(12).minNumberOfRepayments(6).maxNumberOfRepayments(24)
|
||||||
|
.repaymentEvery(1).repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L).interestRatePerPeriod(10.0)
|
||||||
|
.minInterestRatePerPeriod(0.0).maxInterestRatePerPeriod(120.0).interestRateFrequencyType(InterestRateFrequencyType.YEARS)
|
||||||
|
.amortizationType(AmortizationType.EQUAL_INSTALLMENTS).interestType(InterestType.DECLINING_BALANCE)
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).allowPartialPeriodInterestCalcualtion(false)
|
||||||
|
.transactionProcessingStrategyCode("advanced-payment-allocation-strategy")
|
||||||
|
.paymentAllocation(List.of(createDefaultPaymentAllocation("NEXT_INSTALLMENT"))).creditAllocation(List.of())
|
||||||
|
.daysInMonthType(30).daysInYearType(360).isInterestRecalculationEnabled(false).accountingRule(3) // Accrual-based
|
||||||
|
// accounting
|
||||||
|
// GL Account Mappings for Accrual-Based Accounting
|
||||||
|
.fundSourceAccountId(fundSource.getAccountID().longValue())
|
||||||
|
.loanPortfolioAccountId(loansReceivableAccount.getAccountID().longValue())
|
||||||
|
.transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())
|
||||||
|
.interestOnLoanAccountId(interestIncomeAccount.getAccountID().longValue())
|
||||||
|
.incomeFromFeeAccountId(feeIncomeAccount.getAccountID().longValue())
|
||||||
|
.incomeFromPenaltyAccountId(penaltyIncomeAccount.getAccountID().longValue())
|
||||||
|
.incomeFromRecoveryAccountId(recoveriesAccount.getAccountID().longValue())
|
||||||
|
.writeOffAccountId(writtenOffAccount.getAccountID().longValue())
|
||||||
|
.overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())
|
||||||
|
// Receivable accounts required for accrual-based accounting
|
||||||
|
.receivableInterestAccountId(interestReceivableAccount.getAccountID().longValue())
|
||||||
|
.receivableFeeAccountId(feeReceivableAccount.getAccountID().longValue())
|
||||||
|
.receivablePenaltyAccountId(penaltyReceivableAccount.getAccountID().longValue())
|
||||||
|
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())
|
||||||
|
.incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue())
|
||||||
|
.deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue()).loanScheduleType("PROGRESSIVE")
|
||||||
|
.loanScheduleProcessingType("HORIZONTAL").enableBuyDownFee(true)
|
||||||
|
.buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
|
||||||
|
.buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
|
||||||
|
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE).locale("en").dateFormat("dd MMMM yyyy");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to add buy down fee for a loan
|
||||||
|
*
|
||||||
|
* @param loanId
|
||||||
|
* the ID of the loan to add the buy down fee to
|
||||||
|
* @param amount
|
||||||
|
* the amount of the buy down fee
|
||||||
|
* @param date
|
||||||
|
* the transaction date in format specified by DATETIME_PATTERN
|
||||||
|
* @return the ID of the created buy down fee transaction
|
||||||
|
*/
|
||||||
|
private Long addBuyDownFeeForLoan(Long loanId, Double amount, String date) {
|
||||||
|
String buyDownFeeExternalId = UUID.randomUUID().toString();
|
||||||
|
PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeLoanBuyDownFee(loanId,
|
||||||
|
new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(date).locale("en")
|
||||||
|
.transactionAmount(amount).externalId(buyDownFeeExternalId).note("Buy Down Fee Transaction"));
|
||||||
|
return response.getResourceId();
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -166,6 +166,8 @@ public class LoanProductTestBuilder {
|
|||||||
private List<String> supportedInterestRefundTypes = null;
|
private List<String> supportedInterestRefundTypes = null;
|
||||||
private String chargeOffBehaviour;
|
private String chargeOffBehaviour;
|
||||||
private boolean interestRecognitionOnDisbursementDate = false;
|
private boolean interestRecognitionOnDisbursementDate = false;
|
||||||
|
private Boolean enableBuyDownFee = false;
|
||||||
|
private String buyDownFeeCalculationType;
|
||||||
|
|
||||||
public String build() {
|
public String build() {
|
||||||
final HashMap<String, Object> map = build(null, null);
|
final HashMap<String, Object> map = build(null, null);
|
||||||
@@ -341,6 +343,14 @@ public class LoanProductTestBuilder {
|
|||||||
map.put("chargeOffBehaviour", chargeOffBehaviour);
|
map.put("chargeOffBehaviour", chargeOffBehaviour);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.enableBuyDownFee != null) {
|
||||||
|
map.put("enableBuyDownFee", this.enableBuyDownFee);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.buyDownFeeCalculationType != null) {
|
||||||
|
map.put("buyDownFeeCalculationType", this.buyDownFeeCalculationType);
|
||||||
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,4 +916,13 @@ public class LoanProductTestBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LoanProductTestBuilder withEnableBuyDownFee(final Boolean enableBuyDownFee) {
|
||||||
|
this.enableBuyDownFee = enableBuyDownFee;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoanProductTestBuilder withBuyDownFeeCalculationType(final String buyDownFeeCalculationType) {
|
||||||
|
this.buyDownFeeCalculationType = buyDownFeeCalculationType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -3049,6 +3049,10 @@ public class LoanTransactionHelper {
|
|||||||
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction(loanId, request, "downPayment"));
|
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction(loanId, request, "downPayment"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PostLoansLoanIdTransactionsResponse makeLoanBuyDownFee(Long loanId, PostLoansLoanIdTransactionsRequest request) {
|
||||||
|
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction(loanId, request, "buyDownFee"));
|
||||||
|
}
|
||||||
|
|
||||||
public List<AdvancedPaymentData> getAdvancedPaymentAllocationRules(final Integer loanId) {
|
public List<AdvancedPaymentData> getAdvancedPaymentAllocationRules(final Integer loanId) {
|
||||||
return Calls.ok(FineractClientHelper.getFineractClient().legacy.getAdvancedPaymentAllocationRulesOfLoan(loanId.longValue()));
|
return Calls.ok(FineractClientHelper.getFineractClient().legacy.getAdvancedPaymentAllocationRulesOfLoan(loanId.longValue()));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user