FINERACT-2082: New Transaction Type - Interest Payment Waiver

This commit is contained in:
Soma Sörös
2024-06-21 11:40:21 +02:00
committed by Adam Saghy
parent 1c53fe2136
commit 9152408c24
29 changed files with 2809 additions and 50 deletions
@@ -842,6 +842,15 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder loanInterestPaymentWaiverTransaction(final Long loanId) {
this.actionName = "INTERESTPAYMENTWAIVER";
this.entityName = "LOAN";
this.entityId = null;
this.loanId = loanId;
this.href = "/loans/" + loanId + "/transactions/template?command=interestpaymentwaiver";
return this;
}
public CommandWrapperBuilder refundLoanCharge(final Long loanId) {
this.actionName = "CHARGEREFUND";
this.entityName = "LOAN";
@@ -0,0 +1,35 @@
/**
* 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.infrastructure.event.business.domain.loan.transaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public class LoanTransactionInterestPaymentWaiverPostBusinessEvent extends LoanTransactionBusinessEvent {
private static final String TYPE = "LoanTransactionInterestPaymentWaiverPostBusinessEvent";
public LoanTransactionInterestPaymentWaiverPostBusinessEvent(LoanTransaction value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
}
@@ -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.infrastructure.event.business.domain.loan.transaction;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusinessEvent;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
public class LoanTransactionInterestPaymentWaiverPreBusinessEvent extends LoanBusinessEvent {
private static final String TYPE = "LoanTransactionInterestPaymentWaiverPreBusinessEvent";
public LoanTransactionInterestPaymentWaiverPreBusinessEvent(Loan value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
}
@@ -37,6 +37,7 @@ public class LoanTransactionEnumData {
private final boolean merchantIssuedRefund;
private final boolean payoutRefund;
private final boolean goodwillCredit;
private final boolean interestPaymentWaiver;
private final boolean chargeRefund;
private final boolean contra;
private final boolean waiveInterest;
@@ -88,6 +89,7 @@ public class LoanTransactionEnumData {
this.chargeAdjustment = Long.valueOf(26).equals(this.id);
this.chargeoff = Long.valueOf(27).equals(this.id);
this.downPayment = Long.valueOf(28).equals(this.id);
this.interestPaymentWaiver = Long.valueOf(31).equals(this.id);
this.reAge = Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id);
this.reAmortize = Long.valueOf(LoanTransactionType.REAMORTIZE.getValue()).equals(this.id);
}
@@ -1387,7 +1387,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
}
private void updateLoanSummaryDerivedFields() {
public void updateLoanSummaryDerivedFields() {
if (isNotDisbursed()) {
this.summary.zeroFields();
this.totalOverpaid = null;
@@ -2586,9 +2586,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransaction, LoanEvent event) {
if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isMerchantIssuedRefund()
|| repaymentTransaction.isPayoutRefund() || repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment()
|| repaymentTransaction.isDownPayment()) {
if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isInterestPaymentWaiver()
|| repaymentTransaction.isMerchantIssuedRefund() || repaymentTransaction.isPayoutRefund()
|| repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment() || repaymentTransaction.isDownPayment()) {
if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid";
@@ -2799,7 +2800,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return loanTransactionDate;
}
private LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate dueDate) {
public LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate dueDate) {
return getRepaymentScheduleInstallments().stream() //
.filter(installment -> dueDate.equals(installment.getDueDate())).findFirst() //
.orElse(null);
@@ -2887,7 +2888,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
processIncomeAccrualTransactionOnLoanClosure();
}
private void processIncomeAccrualTransactionOnLoanClosure() {
public void processIncomeAccrualTransactionOnLoanClosure() {
if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()
&& this.getStatus().isClosedObligationsMet() && !isNpa() && !isChargedOff()) {
@@ -3161,7 +3162,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.orElse(null);
}
private boolean isOverPaid() {
public boolean isOverPaid() {
return calculateTotalOverpayment().isGreaterThanZero();
}
@@ -3209,7 +3210,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return getTotalRecoveredPayments();
}
private MonetaryCurrency loanCurrency() {
public MonetaryCurrency loanCurrency() {
return this.loanRepaymentScheduleDetail.getCurrency();
}
@@ -3479,7 +3480,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return getStatus().isClosed() || isCancelled();
}
private boolean isClosedObligationsMet() {
public boolean isClosedObligationsMet() {
return getStatus().isClosedObligationsMet();
}
@@ -6024,7 +6025,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
}
private boolean isForeclosure() {
public boolean isForeclosure() {
boolean isForeClosure = false;
if (this.loanSubStatus != null) {
isForeClosure = LoanSubStatus.fromInt(loanSubStatus).isForeclosed();
@@ -580,11 +580,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
public boolean isRepaymentLikeType() {
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()
|| isChargeAdjustment() || isDownPayment();
|| isChargeAdjustment() || isDownPayment() || isInterestPaymentWaiver();
}
public boolean isTypeAllowedForChargeback() {
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isDownPayment();
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isDownPayment()
|| isInterestPaymentWaiver();
}
public boolean isRepayment() {
@@ -607,6 +608,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
return LoanTransactionType.GOODWILL_CREDIT.equals(getTypeOf()) && isNotReversed();
}
public boolean isInterestPaymentWaiver() {
return LoanTransactionType.INTEREST_PAYMENT_WAIVER.equals(getTypeOf()) && isNotReversed();
}
public boolean isChargeRefund() {
return LoanTransactionType.CHARGE_REFUND.equals(getTypeOf()) && isNotReversed();
}
@@ -63,6 +63,7 @@ public enum LoanTransactionType {
DOWN_PAYMENT(28, "loanTransactionType.downPayment"), //
REAGE(29, "loanTransactionType.reAge"), //
REAMORTIZE(30, "loanTransactionType.reAmortize"), //
INTEREST_PAYMENT_WAIVER(31, "loanTransactionType.interestPaymentWaiver"), //
;
private final Integer value;
@@ -109,6 +110,7 @@ public enum LoanTransactionType {
case 28 -> LoanTransactionType.DOWN_PAYMENT;
case 29 -> LoanTransactionType.REAGE;
case 30 -> LoanTransactionType.REAMORTIZE;
case 31 -> LoanTransactionType.INTEREST_PAYMENT_WAIVER;
default -> LoanTransactionType.INVALID;
};
}
@@ -133,6 +135,10 @@ public enum LoanTransactionType {
return this.equals(LoanTransactionType.REPAYMENT);
}
public boolean isInterestPaymentWaiver() {
return this.equals(LoanTransactionType.INTEREST_PAYMENT_WAIVER);
}
public boolean isMerchantIssuedRefund() {
return this.equals(LoanTransactionType.MERCHANT_ISSUED_REFUND);
}
@@ -150,7 +156,8 @@ public enum LoanTransactionType {
}
public boolean isRepaymentType() {
return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() || isDownPayment());
return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() || isDownPayment()
|| isInterestPaymentWaiver());
}
public boolean isRecoveryRepayment() {
@@ -52,6 +52,9 @@ public interface LoanWritePlatformService {
CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType repaymentTransactionType, Long loanId,
JsonCommand command, boolean isRecoveryRepayment, String chargeRefundChargeType);
@Transactional
CommandProcessingResult makeInterestPaymentWaiver(JsonCommand command);
Map<String, Object> makeLoanBulkRepayment(CollectionSheetBulkRepaymentCommand bulkRepaymentCommand);
CommandProcessingResult adjustLoanTransaction(Long loanId, Long transactionId, JsonCommand command);
@@ -40,7 +40,8 @@ public enum PaymentAllocationTransactionType {
CHARGE_ADJUSTMENT(LoanTransactionType.CHARGE_ADJUSTMENT, "Charge adjustment"), //
WAIVE_INTEREST(LoanTransactionType.WAIVE_INTEREST, "Waive interest"), //
CHARGE_PAYMENT(LoanTransactionType.CHARGE_PAYMENT, "Charge payment"), //
REFUND_FOR_ACTIVE_LOAN(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, "Refund for active loan");
REFUND_FOR_ACTIVE_LOAN(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, "Refund for active loan"), //
INTEREST_PAYMENT_WAIVER(LoanTransactionType.INTEREST_PAYMENT_WAIVER, "Interest payment waiver");
private final LoanTransactionType loanTransactionType;
private final String humanReadableName;
@@ -303,6 +303,8 @@ public final class LoanEnumerations {
LoanTransactionType.PAYOUT_REFUND.getCode(), "Payout Refund");
case GOODWILL_CREDIT -> new LoanTransactionEnumData(LoanTransactionType.GOODWILL_CREDIT.getValue().longValue(),
LoanTransactionType.GOODWILL_CREDIT.getCode(), "Goodwill Credit");
case INTEREST_PAYMENT_WAIVER -> new LoanTransactionEnumData(LoanTransactionType.INTEREST_PAYMENT_WAIVER.getValue().longValue(),
LoanTransactionType.INTEREST_PAYMENT_WAIVER.getCode(), "Interest Payment Waiver");
case CHARGE_REFUND -> new LoanTransactionEnumData(LoanTransactionType.CHARGE_REFUND.getValue().longValue(),
LoanTransactionType.CHARGE_REFUND.getCode(), "Charge Refund");
case CHARGEBACK -> new LoanTransactionEnumData(LoanTransactionType.CHARGEBACK.getValue().longValue(),
@@ -189,7 +189,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
case CREDIT_BALANCE_REFUND ->
handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER ->
handleRepayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(), ctx.getOverpaymentHolder());
case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(),
@@ -110,6 +110,106 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
else if (loanTransactionDTO.getTransactionType().isChargeoff()) {
createJournalEntriesForChargeOff(loanDTO, loanTransactionDTO, office);
}
// Logic for Interest Payment Waiver
else if (loanTransactionDTO.getTransactionType().isInterestPaymentWaiver()) {
createJournalEntriesForInterestPaymentWaiver(loanDTO, loanTransactionDTO, office);
}
}
}
private void createJournalEntriesForInterestPaymentWaiver(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) {
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
final String currencyCode = loanDTO.getCurrencyCode();
final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff();
// transaction properties
final String transactionId = loanTransactionDTO.getTransactionId();
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
final BigDecimal feesAmount = loanTransactionDTO.getFees();
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
final BigDecimal overPayment = loanTransactionDTO.getOverPayment();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
final boolean isReversal = loanTransactionDTO.isReversed();
Map<GLAccount, BigDecimal> accountMapForCredit = new LinkedHashMap<>();
Map<Integer, BigDecimal> accountMapForDebit = new LinkedHashMap<>();
if (isMarkedAsChargeOff) {
// ChargeOFF
// principal payment
if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId,
AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
}
// interest payment
if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) {
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId,
AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
}
// handle fees payment
if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) {
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId,
AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
}
// handle penalty payment
if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) {
populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId,
AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
}
// handle overpayment
if (overPayment != null && overPayment.compareTo(BigDecimal.ZERO) > 0) {
populateCreditDebitMaps(loanProductId, overPayment, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(),
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
}
} else {
// principal payment
if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
}
// interest payment
if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) {
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(),
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
}
// handle fees payment
if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) {
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(),
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
}
// handle penalty payment
if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) {
populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId,
AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(),
accountMapForCredit, accountMapForDebit);
}
// handle overpayment
if (overPayment != null && overPayment.compareTo(BigDecimal.ZERO) > 0) {
populateCreditDebitMaps(loanProductId, overPayment, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(),
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
}
}
// create credit entries
for (Map.Entry<GLAccount, BigDecimal> creditEntry : accountMapForCredit.entrySet()) {
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate,
creditEntry.getValue(), isReversal, creditEntry.getKey());
}
// create debit entries
for (Map.Entry<Integer, BigDecimal> debitEntry : accountMapForDebit.entrySet()) {
this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue(), isReversal);
}
}
@@ -111,9 +111,9 @@ public class LoanTransactionsApiResource {
+ "loans/1/transactions/template?command=disburse" + "\n" + "loans/1/transactions/template?command=disburseToSavings" + "\n"
+ "loans/1/transactions/template?command=recoverypayment" + "\n" + "loans/1/transactions/template?command=prepayLoan" + "\n"
+ "loans/1/transactions/template?command=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n"
+ "loans/1/transactions/template?command=foreclosure" + "\n"
+ "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)" + "\n"
+ "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
+ "loans/1/transactions/template?command=foreclosure" + "\n" + "loans/1/transactions/template?command=interestPaymentWaiver"
+ "\n" + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)"
+ "\n" + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
public String retrieveTransactionTemplate(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
@@ -140,9 +140,9 @@ public class LoanTransactionsApiResource {
+ "loans/1/transactions/template?command=disburse" + "\n" + "loans/1/transactions/template?command=disburseToSavings" + "\n"
+ "loans/1/transactions/template?command=recoverypayment" + "\n" + "loans/1/transactions/template?command=prepayLoan" + "\n"
+ "loans/1/transactions/template?command=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n"
+ "loans/1/transactions/template?command=foreclosure" + "\n"
+ "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)" + "\n"
+ "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
+ "loans/1/transactions/template?command=foreclosure" + "\n" + "loans/1/transactions/template?command=interestPaymentWaiver"
+ "\n" + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)"
+ "\n" + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
public String retrieveTransactionTemplate(
@@ -455,6 +455,8 @@ public class LoanTransactionsApiResource {
commandRequest = builder.loanPayoutRefundTransaction(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, "goodwillCredit")) {
commandRequest = builder.loanGoodwillCreditTransaction(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, "interestPaymentWaiver")) {
commandRequest = builder.loanInterestPaymentWaiverTransaction(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, "chargeRefund")) {
commandRequest = builder.refundLoanCharge(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, "waiveinterest")) {
@@ -521,6 +523,10 @@ public class LoanTransactionsApiResource {
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.GOODWILL_CREDIT,
resolvedLoanId, transactionDate);
} else if (CommandParameterUtil.is(commandParam, "interestPaymentWaiver")) {
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.INTEREST_PAYMENT_WAIVER,
resolvedLoanId, transactionDate);
} else if (CommandParameterUtil.is(commandParam, "waiveinterest")) {
transactionData = this.loanReadPlatformService.retrieveWaiveInterestDetails(resolvedLoanId);
} else if (CommandParameterUtil.is(commandParam, "writeoff")) {
@@ -57,6 +57,8 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionDownPaymentPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionGoodwillCreditPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionGoodwillCreditPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMakeRepaymentPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMakeRepaymentPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMerchantIssuedRefundPostBusinessEvent;
@@ -292,6 +294,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
repaymentEvent = new LoanTransactionPayoutRefundPreBusinessEvent(loan);
} else if (repaymentTransactionType.isGoodwillCredit()) {
repaymentEvent = new LoanTransactionGoodwillCreditPreBusinessEvent(loan);
} else if (repaymentTransactionType.isInterestPaymentWaiver()) {
repaymentEvent = new LoanTransactionInterestPaymentWaiverPreBusinessEvent(loan);
} else if (repaymentTransactionType.isChargeRefund()) {
repaymentEvent = new LoanChargePaymentPreBusinessEvent(loan);
} else if (isRecoveryRepayment) {
@@ -313,6 +317,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
repaymentEvent = new LoanTransactionPayoutRefundPostBusinessEvent(transaction);
} else if (repaymentTransactionType.isGoodwillCredit()) {
repaymentEvent = new LoanTransactionGoodwillCreditPostBusinessEvent(transaction);
} else if (repaymentTransactionType.isInterestPaymentWaiver()) {
repaymentEvent = new LoanTransactionInterestPaymentWaiverPostBusinessEvent(transaction);
} else if (repaymentTransactionType.isChargeRefund()) {
repaymentEvent = new LoanChargePaymentPostBusinessEvent(transaction);
} else if (isRecoveryRepayment) {
@@ -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.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.LoanWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@CommandType(entity = "LOAN", action = "INTERESTPAYMENTWAIVER")
public class LoanInterestPaymentWaiverCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.makeInterestPaymentWaiver(command);
}
}
@@ -35,32 +35,54 @@ import java.util.Set;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
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.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.service.HolidayUtil;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepository;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.exception.CurrencyNotFoundException;
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
import org.apache.fineract.portfolio.calendar.exception.NotValidRecurringDateException;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
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.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanRepaymentScheduleNotFoundException;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.springframework.stereotype.Component;
@Component
@AllArgsConstructor
public final class LoanEventApiJsonValidator {
public final class LoanTransactionValidator {
private final FromJsonHelper fromApiJsonHelper;
private final LoanApplicationValidator fromApiJsonDeserializer;
private final LoanRepository loanRepository;
private final ApplicationCurrencyRepository applicationCurrencyRepository;
private final LoanUtilService loanUtilService;
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
@@ -562,4 +584,154 @@ public final class LoanEventApiJsonValidator {
validatePaymentDetails(baseDataValidator, element);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateLoanClientIsActive(final Loan loan) {
final Client client = loan.client();
if (client != null && client.isNotActive()) {
throw new ClientNotActiveException(client.getId());
}
}
public void validateLoanGroupIsActive(final Loan loan) {
final Group group = loan.group();
if (group != null && group.isNotActive()) {
throw new GroupNotActiveException(group.getId());
}
}
public void validateLoanStatusIsActiveOrFullyPaidOrOverpaid(final Loan loan) {
if (!(loan.isOpen() || loan.isClosedObligationsMet() || loan.isOverPaid())) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid";
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.must.be.active.fully.paid.or.overpaid",
defaultUserMessage);
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
public void validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(Loan loan, LocalDate transactionDate,
String reversedOrCreated) {
for (LoanTransaction txn : loan.getLoanTransactions()) {
if (txn.isChargeRefund() && DateUtils.isBefore(transactionDate, txn.getTransactionDate())) {
final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists";
final String details = "Loan Transaction: " + loan.getId() + " Can't be " + reversedOrCreated
+ " because a Later Charge Refund Exists.";
throw new LoanChargeRefundException(errorMessage, details);
}
}
}
public void validateLoanDisbursementIsBeforeTransactionDate(final Loan loan, final LocalDate transactionDate) {
if (DateUtils.isBefore(transactionDate, loan.getDisbursementDate())) {
final String errorMessage = "The transaction date cannot be before the loan disbursement date: "
+ loan.getDisbursementDate().toString();
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage,
transactionDate, loan.getDisbursementDate());
}
}
public void validateTransactionShouldNotBeInTheFuture(final LocalDate transactionDate) {
if (DateUtils.isDateInTheFuture(transactionDate)) {
final String errorMessage = "The transaction date cannot be in the future.";
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, transactionDate);
}
}
public void validateLoanHasCurrency(final Loan loan) {
MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency defaultApplicationCurrency = this.applicationCurrencyRepository.findOneByCode(currency.getCode());
if (defaultApplicationCurrency == null) {
throw new CurrencyNotFoundException(currency.getCode());
}
}
public void validateClientOfficeJoiningDateIsBeforeTransactionDate(Loan loan, LocalDate transactionDate) {
if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) {
final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate();
if (DateUtils.isBefore(transactionDate, clientOfficeJoiningDate)) {
String errorMessage = "The date on which a repayment or waiver is made cannot be earlier than client's transfer date to this office";
String action = "repayment.or.waiver";
String postfix = "cannot.be.made.before.client.transfer.date";
throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, clientOfficeJoiningDate);
}
}
}
public void validateActivityNotBeforeLastTransactionDate(final Loan loan, final LocalDate activityDate) {
if (!(loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || loan.loanProduct().isHoldGuaranteeFunds())) {
return;
}
LocalDate lastTransactionDate = loan.getLastUserTransactionDate();
if (DateUtils.isAfter(lastTransactionDate, activityDate)) {
String errorMessage = "The date on which a repayment or waiver is made cannot be earlier than last transaction date";
String action = "repayment.or.waiver";
String postfix = "cannot.be.made.before.last.transaction.date";
throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, lastTransactionDate);
}
}
public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays,
final boolean allowTransactionsOnNonWorkingDay) {
if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) {
final String errorMessage = "Repayment date cannot be on a non working day";
throw new LoanApplicationDateException("repayment.date.on.non.working.day", errorMessage, repaymentDate);
}
}
public void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean allowTransactionsOnHoliday,
final List<Holiday> holidays) {
if (!allowTransactionsOnHoliday && HolidayUtil.isHoliday(repaymentDate, holidays)) {
final String errorMessage = "Repayment date cannot be on a holiday";
throw new LoanApplicationDateException("repayment.date.on.holiday", errorMessage, repaymentDate);
}
}
public void validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(Loan loan) {
if (loan.getLoanProduct().isMultiDisburseLoan()) {
BigDecimal totalDisbursed = loan.getDisbursedAmount();
BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0) {
final String errorMessage = "The transaction amount cannot exceed threshold.";
throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
}
}
}
public void validateLoanTransactionInterestPaymentWaiver(JsonCommand command) {
final Long loanId = command.getLoanId();
Loan loan = this.loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
validateNewRepaymentTransaction(command.json());
validateTransactionShouldNotBeInTheFuture(transactionDate);
validateLoanClientIsActive(loan);
validateLoanHasCurrency(loan);
validateLoanGroupIsActive(loan);
validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan);
validateLoanDisbursementIsBeforeTransactionDate(loan, transactionDate);
validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(loan, transactionDate, "created");
validateClientOfficeJoiningDateIsBeforeTransactionDate(loan, transactionDate);
validateActivityNotBeforeLastTransactionDate(loan, transactionDate);
HolidayDetailDTO holidayDetailDTO = loanUtilService.constructHolidayDTO(loan.getOfficeId(), loan.getDisbursementDate());
validateRepaymentDateIsOnHoliday(transactionDate, holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
validateRepaymentDateIsOnNonWorkingDay(transactionDate, holidayDetailDTO.getWorkingDays(),
holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan);
}
public void validateLoanTransactionInterestPaymentWaiverAfterRecalculation(Loan loan) {
// Payment allocation calculates the new total principal repaid, and it should be validated after recalculation
if (loan.getLoanProduct().isMultiDisburseLoan()) {
BigDecimal totalDisbursed = loan.getDisbursedAmount();
BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0
&& loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) {
final String errorMessage = "The transaction amount cannot exceed threshold.";
throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
}
}
}
}
@@ -0,0 +1,68 @@
/**
* 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.Map;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
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.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
@RequiredArgsConstructor
public class LoanTransactionAssembler {
private final ExternalIdFactory externalIdFactory;
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
LoanTransaction assembleTransactionAndCalculateChanges(Loan loan, JsonCommand command, Map<String, Object> changes) {
final String noteText = command.stringValueOfParameterNamed("note");
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat());
changes.put("paymentTypeId", command.longValueOfParameterNamed("paymentTypeId"));
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
}
if (!txnExternalId.isEmpty()) {
changes.put(LoanApiConstants.externalIdParameterName, txnExternalId);
}
final PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
final LoanTransactionType repaymentTransactionType = LoanTransactionType.INTEREST_PAYMENT_WAIVER;
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
final Money repaymentAmount = Money.of(loan.getCurrency(), transactionAmount);
return LoanTransaction.repaymentType(repaymentTransactionType, loan.getOffice(), repaymentAmount, paymentDetail, transactionDate,
txnExternalId, null);
}
}
@@ -175,6 +175,17 @@ public class LoanUtilService {
return holidayDetailDTO;
}
public HolidayDetailDTO constructHolidayDTO(final Long officeId, LocalDate localDate) {
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, localDate,
HolidayStatusType.ACTIVE.getValue());
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled();
final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
return new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay);
}
private FloatingRateDTO constructFloatingRateDTO(final Loan loan) {
FloatingRateDTO floatingRateDTO = null;
if (loan.loanProduct().isLinkedToFloatingInterestRate()) {
@@ -84,6 +84,9 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoChargeOffBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWaiveInterestBusinessEvent;
@@ -109,7 +112,9 @@ import org.apache.fineract.portfolio.account.domain.AccountAssociationsRepositor
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
@@ -162,7 +167,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException;
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
@@ -180,7 +188,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
@@ -188,6 +196,7 @@ import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.data.PostDatedChecksStatus;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.service.RepaymentWithPostDatedChecksAssembler;
@@ -203,7 +212,7 @@ import org.springframework.transaction.annotation.Transactional;
public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService {
private final PlatformSecurityContext context;
private final LoanEventApiJsonValidator loanEventApiJsonValidator;
private final LoanTransactionValidator loanTransactionValidator;
private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanAccountDomainService loanAccountDomainService;
@@ -247,6 +256,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final ErrorHandler errorHandler;
private final LoanDownPaymentHandlerService loanDownPaymentHandlerService;
private final AccountTransferRepository accountTransferRepository;
private final LoanTransactionAssembler loanTransactionAssembler;
@Transactional
@Override
@@ -283,11 +294,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final AppUser currentUser = getAppUserIfPresent();
this.loanEventApiJsonValidator.validateDisbursement(command.json(), isAccountTransfer);
this.loanTransactionValidator.validateDisbursement(command.json(), isAccountTransfer);
if (command.parameterExists("postDatedChecks")) {
// validate with post dated checks for the disbursement
this.loanEventApiJsonValidator.validateDisbursementWithPostDatedChecks(command.json(), loanId);
this.loanTransactionValidator.validateDisbursementWithPostDatedChecks(command.json(), loanId);
}
Loan loan = this.loanAssembler.assembleFrom(loanId);
@@ -364,7 +375,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
if (loan.isSyncDisbursementWithMeeting()) {
this.loanEventApiJsonValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance,
this.loanTransactionValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance,
scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), scheduleGeneratorDTO.getNumberOfdays());
}
@@ -956,13 +967,307 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
chargeRefundChargeType);
}
private void handleLoanOverpayment(Loan loan, LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
loan.setOverpaidOnDate(transactionDate);
loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, loan);
loan.setClosedOnDate(null);
loan.setActualMaturityDate(null);
}
private void handleLoanRepaymentInFull(final Loan loan, final LocalDate transactionDate,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
boolean isAllChargesPaid = true;
for (final LoanCharge loanCharge : loan.getCharges()) {
if (loanCharge.isActive() && loanCharge.amount().compareTo(BigDecimal.ZERO) > 0
&& !(loanCharge.isPaid() || loanCharge.isWaived())) {
isAllChargesPaid = false;
break;
}
}
if (isAllChargesPaid) {
loan.setClosedOnDate(transactionDate);
loan.setActualMaturityDate(transactionDate);
loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, loan);
} else if (LoanStatus.fromInt(loan.getLoanStatus()).isOverpaid()) {
if (loan.getTotalOverpaid() == null || BigDecimal.ZERO.compareTo(loan.getTotalOverpaid()) == 0) {
loan.setOverpaidOnDate(null);
}
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
}
loan.processIncomeAccrualTransactionOnLoanClosure();
}
private boolean doPostLoanTransactionChecks(final Loan loan, final LocalDate transactionDate,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
boolean statusChanged = false;
boolean isOverpaid = loan.getTotalOverpaid() != null && loan.getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0;
if (isOverpaid) {
// FIXME - kw - update account balance to negative amount.
handleLoanOverpayment(loan, transactionDate, loanLifecycleStateMachine);
statusChanged = true;
} else if (loan.getSummary().isRepaidInFull(loan.loanCurrency())) {
handleLoanRepaymentInFull(loan, transactionDate, loanLifecycleStateMachine);
statusChanged = true;
} else {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
}
if (loan.getTotalOverpaid() == null || BigDecimal.ZERO.compareTo(loan.getTotalOverpaid()) == 0) {
loan.setOverpaidOnDate(null);
}
return statusChanged;
}
private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
if (transferTransaction != null) {
transferTransaction.updateToLoanTransaction(newLoanTransaction);
this.accountTransferRepository.save(transferTransaction);
}
}
// TODO check
public boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction,
final List<LoanTransaction> loanTransactions) {
boolean isChronologicallyLatestRepaymentOrWaiver = true;
final LocalDate currentTransactionDate = loanTransaction.getTransactionDate();
for (final LoanTransaction previousTransaction : loanTransactions) {
if (!previousTransaction.isDisbursement() && previousTransaction.isNotReversed()
&& (DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate())
|| (DateUtils.isEqual(currentTransactionDate, previousTransaction.getTransactionDate())
&& ((loanTransaction.getId() == null && previousTransaction.getId() == null)
|| (loanTransaction.getId() != null && (previousTransaction.getId() == null
|| loanTransaction.getId().compareTo(previousTransaction.getId()) < 0)))))) {
isChronologicallyLatestRepaymentOrWaiver = false;
break;
}
}
return isChronologicallyLatestRepaymentOrWaiver;
}
private ChangedTransactionDetail recalculateLoanWithInterestPaymentWaiverTxn(Loan loan,
LoanTransaction newInterestPaymentWaiverTransaction) {
LocalDate recalculateFrom = null;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
recalculateFrom = newInterestPaymentWaiverTransaction.getTransactionDate();
}
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, null);
ChangedTransactionDetail changedTransactionDetail = null;
newInterestPaymentWaiverTransaction.updateLoan(loan);
final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(newInterestPaymentWaiverTransaction,
loan.getLoanTransactions());
if (newInterestPaymentWaiverTransaction.isNotZero(loan.getLoanRepaymentScheduleDetail().getCurrency())) {
loan.addLoanTransaction(newInterestPaymentWaiverTransaction);
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessorFactory()
.determineProcessor(loan.getTransactionProcessingStrategyCode());
final LoanRepaymentScheduleInstallment currentInstallment = loan
.fetchLoanRepaymentScheduleInstallment(newInterestPaymentWaiverTransaction.getTransactionDate());
boolean reprocess = true;
if (!loan.isForeclosure() && isTransactionChronologicallyLatest
&& DateUtils.isEqualBusinessDate(newInterestPaymentWaiverTransaction.getTransactionDate()) && currentInstallment != null
&& currentInstallment.getTotalOutstanding(loan.getCurrency())
.isEqualTo(newInterestPaymentWaiverTransaction.getAmount(loan.getCurrency()))) {
reprocess = false;
}
if (isTransactionChronologicallyLatest && (!reprocess || !loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled())
&& !loan.isForeclosure()) {
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(newInterestPaymentWaiverTransaction,
new LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney())));
reprocess = false;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
reprocess = true;
} else {
final LoanRepaymentScheduleInstallment nextInstallment = loan
.fetchRepaymentScheduleInstallment(currentInstallment.getInstallmentNumber() + 1);
if (nextInstallment != null && nextInstallment.getTotalPaidInAdvance(loan.getCurrency()).isGreaterThanZero()) {
reprocess = true;
}
}
}
}
if (reprocess) {
changedTransactionDetail = reprocessChangedLoanTransactions(loan, loanRepaymentScheduleTransactionProcessor,
scheduleGeneratorDTO);
}
loan.updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(loan, newInterestPaymentWaiverTransaction.getTransactionDate(), defaultLoanLifecycleStateMachine);
return changedTransactionDetail;
}
private ChangedTransactionDetail reprocessChangedLoanTransactions(Loan loan,
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
ScheduleGeneratorDTO scheduleGeneratorDTO) {
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(loan);
}
/***
* Commented since throwing exception if external id present for one of the transactions. for this need to save
* the reversed transactions first and then new transactions.
*/
loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values());
return changedTransactionDetail;
}
@Transactional
@Override
public CommandProcessingResult makeInterestPaymentWaiver(final JsonCommand command) {
loanTransactionValidator.validateLoanTransactionInterestPaymentWaiver(command);
final Long loanId = command.getLoanId();
Loan loan = this.loanAssembler.assembleFrom(loanId);
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionInterestPaymentWaiverPreBusinessEvent(loan));
// save already existing transaction ids
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
final String noteText = command.stringValueOfParameterNamed("note");
final Map<String, Object> changes = new LinkedHashMap<>();
LoanTransaction newInterestPaymentWaiverTransaction = loanTransactionAssembler.assembleTransactionAndCalculateChanges(loan, command,
changes);
ChangedTransactionDetail changedTransactionDetail = recalculateLoanWithInterestPaymentWaiverTxn(loan,
newInterestPaymentWaiverTransaction);
loanTransactionValidator.validateLoanTransactionInterestPaymentWaiverAfterRecalculation(loan);
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newInterestPaymentWaiverTransaction);
saveChangedTransactionDetail(changedTransactionDetail);
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
saveNote(noteText, loan, newInterestPaymentWaiverTransaction);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccountDomainService.recalculateAccruals(loan);
loanAccountDomainService.setLoanDelinquencyTag(loan, newInterestPaymentWaiverTransaction.getTransactionDate());
// disable all active standing orders linked to this loan if status changes to closed
loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan);
updateAndSavePostDatedChecksForIndividualAccount(loan, newInterestPaymentWaiverTransaction);
updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, newInterestPaymentWaiverTransaction);
// Finished Notification
LoanTransactionBusinessEvent transactionRepaymentEvent = new LoanTransactionInterestPaymentWaiverPostBusinessEvent(
newInterestPaymentWaiverTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
.withLoanId(loan.getId()) //
.withEntityId(newInterestPaymentWaiverTransaction.getId()) //
.withEntityExternalId(newInterestPaymentWaiverTransaction.getExternalId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.with(changes) //
.build();
}
private void saveNote(String noteText, Loan loan, LoanTransaction loanTransaction) {
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanTransactionNote(loan, loanTransaction, noteText);
this.noteRepository.save(note);
}
}
private void saveChangedTransactionDetail(ChangedTransactionDetail changedTransactionDetail) {
if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
// Trigger transaction replayed event
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
}
}
private void updateAndSavePostDatedChecksForIndividualAccount(Loan loan, LoanTransaction loanTransaction) {
if (loan.getLoanType().isIndividualAccount()) {
// Mark Post Dated Check as paid.
final Set<LoanTransactionToRepaymentScheduleMapping> loanTransactionToRepaymentScheduleMappings = loanTransaction
.getLoanTransactionToRepaymentScheduleMappings();
if (loanTransactionToRepaymentScheduleMappings != null) {
for (LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping : loanTransactionToRepaymentScheduleMappings) {
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanTransactionToRepaymentScheduleMapping
.getLoanRepaymentScheduleInstallment();
if (loanRepaymentScheduleInstallment != null) {
final boolean isPaid = loanRepaymentScheduleInstallment.isNotFullyPaidOff();
PostDatedChecks postDatedChecks = this.postDatedChecksRepository
.getPendingPostDatedCheck(loanRepaymentScheduleInstallment);
if (postDatedChecks != null) {
if (!isPaid) {
postDatedChecks.setStatus(PostDatedChecksStatus.POST_DATED_CHECKS_PAID);
} else {
postDatedChecks.setStatus(PostDatedChecksStatus.POST_DATED_CHECKS_PENDING);
}
this.postDatedChecksRepository.saveAndFlush(postDatedChecks);
} else {
break;
}
}
}
}
}
}
private void updateAndSaveLoanCollateralTransactionsForIndividualAccounts(Loan loan, LoanTransaction loanTransaction) {
if (loan.getLoanType().isIndividualAccount()) {
Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements();
for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) {
loanCollateralManagement.setLoanTransactionData(loanTransaction);
ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement();
if (loan.getStatus().isClosed()) {
loanCollateralManagement.setIsReleased(true);
BigDecimal quantity = loanCollateralManagement.getQuantity();
clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity));
loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement);
}
}
this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements);
}
}
@Transactional
@Override
public CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final LoanTransactionType repaymentTransactionType,
final Long loanId, final JsonCommand command, final boolean isRecoveryRepayment, final String chargeRefundChargeType) {
this.loanUtilService.validateRepaymentTransactionType(repaymentTransactionType);
this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json());
this.loanTransactionValidator.validateNewRepaymentTransaction(command.json());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
@@ -1083,7 +1388,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) {
this.loanEventApiJsonValidator.validateTransaction(command.json());
this.loanTransactionValidator.validateTransaction(command.json());
LoanTransaction transactionToAdjust = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId())
.orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId()));
@@ -1255,7 +1560,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional
@Override
public CommandProcessingResult chargebackLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) {
this.loanEventApiJsonValidator.validateChargebackTransaction(command.json());
this.loanTransactionValidator.validateChargebackTransaction(command.json());
LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId())
.orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId()));
@@ -1369,7 +1674,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult waiveInterestOnLoan(final Long loanId, final JsonCommand command) {
this.loanEventApiJsonValidator.validateTransaction(command.json());
this.loanTransactionValidator.validateTransaction(command.json());
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
@@ -1456,7 +1761,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
public CommandProcessingResult writeOff(final Long loanId, final JsonCommand command) {
final AppUser currentUser = getAppUserIfPresent();
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
this.loanTransactionValidator.validateTransactionWithNoAmount(command.json());
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
@@ -1534,7 +1839,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult closeLoan(final Long loanId, final JsonCommand command) {
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
this.loanTransactionValidator.validateTransactionWithNoAmount(command.json());
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
@@ -1643,7 +1948,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult closeAsRescheduled(final Long loanId, final JsonCommand command) {
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
this.loanTransactionValidator.validateTransactionWithNoAmount(command.json());
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
@@ -1839,7 +2144,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult loanReassignment(final Long loanId, final JsonCommand command) {
this.loanEventApiJsonValidator.validateUpdateOfLoanOfficer(command.json());
this.loanTransactionValidator.validateUpdateOfLoanOfficer(command.json());
final Long fromLoanOfficerId = command.longValueOfParameterNamed("fromLoanOfficerId");
final Long toLoanOfficerId = command.longValueOfParameterNamed("toLoanOfficerId");
@@ -1875,7 +2180,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult bulkLoanReassignment(final JsonCommand command) {
this.loanEventApiJsonValidator.validateForBulkLoanReassignment(command.json());
this.loanTransactionValidator.validateForBulkLoanReassignment(command.json());
final Long fromLoanOfficerId = command.longValueOfParameterNamed("fromLoanOfficerId");
final Long toLoanOfficerId = command.longValueOfParameterNamed("toLoanOfficerId");
@@ -2412,7 +2717,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
"Update Loan: " + loanId + " disbursement details is not allowed. Loan Account is Charged-off", loanId);
}
LoanDisbursementDetails loanDisbursementDetails = loan.fetchLoanDisbursementsById(disbursementId);
this.loanEventApiJsonValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails);
this.loanTransactionValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails);
CommandProcessingResult result = processLoanDisbursementDetail(loan, loanId, command, loanDisbursementDetails);
businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateDisbursementDataBusinessEvent(loan));
@@ -2506,7 +2811,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command) {
this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
this.loanTransactionValidator.validateNewRefundTransaction(command.json());
Loan loan = this.loanAssembler.assembleFrom(loanId);
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
@@ -2551,7 +2856,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
@Transactional
public CommandProcessingResult markLoanAsFraud(Long loanId, JsonCommand command) {
this.loanEventApiJsonValidator.validateMarkAsFraudLoan(command.json());
this.loanTransactionValidator.validateMarkAsFraudLoan(command.json());
Loan loan = this.loanAssembler.assembleFrom(loanId);
final Map<String, Object> changes = new LinkedHashMap<>();
@@ -2578,7 +2883,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional
public CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command) {
this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
this.loanTransactionValidator.validateNewRefundTransaction(command.json());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
@@ -2696,7 +3001,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(loanId);
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.transactionDateParamName, element);
final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
this.loanEventApiJsonValidator.validateLoanForeclosure(command.json());
this.loanTransactionValidator.validateLoanForeclosure(command.json());
final Map<String, Object> changes = new LinkedHashMap<>();
// Got changed to match with the rest of the APIs
changes.put("dateFormat", command.dateFormat());
@@ -2732,7 +3037,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional
public CommandProcessingResult chargeOff(JsonCommand command) {
loanEventApiJsonValidator.validateChargeOffTransaction(command.json());
loanTransactionValidator.validateChargeOffTransaction(command.json());
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put(LoanApiConstants.transactionDateParamName,
@@ -2807,7 +3112,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
@Transactional
public CommandProcessingResult undoChargeOff(JsonCommand command) {
this.loanEventApiJsonValidator.validateUndoChargeOff(command.json());
this.loanTransactionValidator.validateUndoChargeOff(command.json());
final Long loanId = command.getLoanId();
final Loan loan = this.loanAssembler.assembleFrom(loanId);
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
@@ -43,6 +43,7 @@ import org.apache.fineract.organisation.teller.data.CashierTransactionDataValida
import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.apache.fineract.portfolio.account.domain.AccountAssociationsRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountNumberGenerator;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
@@ -87,7 +88,7 @@ import org.apache.fineract.portfolio.loanaccount.mapper.LoanTransactionRelationM
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformServiceImpl;
@@ -121,6 +122,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl;
@@ -253,6 +255,14 @@ public class LoanAccountConfiguration {
calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper);
}
@Bean
@ConditionalOnMissingBean(LoanTransactionAssembler.class)
public LoanTransactionAssembler loanTransactionAssembler(ExternalIdFactory externalIdFactory,
PaymentDetailWritePlatformService paymentDetailWritePlatformService) {
return new LoanTransactionAssembler(externalIdFactory, paymentDetailWritePlatformService);
}
@Bean
@ConditionalOnMissingBean(LoanCalculateRepaymentPastDueService.class)
public LoanCalculateRepaymentPastDueService loanCalculateRepaymentPastDueService() {
@@ -362,7 +372,7 @@ public class LoanAccountConfiguration {
@Bean
@ConditionalOnMissingBean(LoanWritePlatformService.class)
public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext context,
LoanEventApiJsonValidator loanEventApiJsonValidator,
LoanTransactionValidator loanTransactionValidator,
LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer, LoanRepositoryWrapper loanRepositoryWrapper,
LoanAccountDomainService loanAccountDomainService, NoteRepository noteRepository,
LoanTransactionRepository loanTransactionRepository, LoanTransactionRelationRepository loanTransactionRelationRepository,
@@ -386,8 +396,9 @@ public class LoanAccountConfiguration {
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccountLockService loanAccountLockService,
ExternalIdFactory externalIdFactory, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService,
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ErrorHandler errorHandler,
LoanDownPaymentHandlerService loanDownPaymentHandlerService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanEventApiJsonValidator, loanUpdateCommandFromApiJsonDeserializer,
LoanDownPaymentHandlerService loanDownPaymentHandlerService, AccountTransferRepository accountTransferRepository,
LoanTransactionAssembler loanTransactionAssembler) {
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
paymentDetailWritePlatformService, holidayRepository, configurationDomainService, workingDaysRepository,
@@ -398,7 +409,8 @@ public class LoanAccountConfiguration {
transactionProcessingStrategy, codeValueRepository, cashierTransactionDataValidator, glimRepository, loanRepository,
repaymentWithPostDatedChecksAssembler, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository,
defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService,
loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService);
loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository,
loanTransactionAssembler);
}
@Bean
@@ -160,4 +160,5 @@
<include file="parts/0138_add_external_event_for_loan_reaging_reamortization_2.xml" relativeToChangelogFile="true" />
<include file="parts/0139_add_disburse_without_auto_payment_command.xml" relativeToChangelogFile="true" />
<include file="parts/0140_trial_balance_with_asset_transfer_update.xml" relativeToChangelogFile="true" />
<include file="parts/0141_add_interest_payment_waiver_transaction_type.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,49 @@
<?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.3.xsd">
<changeSet author="fineract" id="1">
<insert tableName="m_permission">
<column name="grouping" value="transaction_loan" />
<column name="code" value="INTERESTPAYMENTWAIVER_LOAN" />
<column name="entity_name" value="LOAN" />
<column name="action_name" value="INTERESTPAYMENTWAIVER" />
<column name="can_maker_checker" valueBoolean="false" />
</insert>
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanTransactionInterestPaymentWaiverPostBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanTransactionInterestPaymentWaiverPreBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
<insert tableName="r_enum_value">
<column name="enum_name" value="transaction_type_enum"/>
<column name="enum_id" valueNumeric="31"/>
<column name="enum_message_property" value="Interest Payment Waiver"/>
<column name="enum_value" value="Interest Payment Waiver"/>
<column name="enum_type" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -101,7 +101,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent",
"LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent",
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent");
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
"LoanTransactionInterestPaymentWaiverPostBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -184,7 +185,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent",
"LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent",
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent");
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
"LoanTransactionInterestPaymentWaiverPostBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -138,6 +138,7 @@ public abstract class BaseLoanIntegrationTest {
protected final Account chargeOffFraudExpenseAccount = accountHelper.createExpenseAccount("chargeOffFraud");
protected final Account writtenOffAccount = accountHelper.createExpenseAccount();
protected final Account goodwillExpenseAccount = accountHelper.createExpenseAccount();
protected final Account goodwillIncomeAccount = accountHelper.createIncomeAccount();
protected final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
protected ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
@@ -710,6 +711,14 @@ public abstract class BaseLoanIntegrationTest {
return response.getResourceId();
}
protected Long addInterestPaymentWaiverForLoan(Long loanId, Double amount, String date) {
String firstRepaymentUUID = UUID.randomUUID().toString();
PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeInterestPaymentWaiver(loanId,
new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(date).locale("en")
.transactionAmount(amount).externalId(firstRepaymentUUID));
return response.getResourceId();
}
protected Long chargeOffLoan(Long loanId, String date) {
String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5);
Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
@@ -235,6 +235,62 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests {
loanProductError.get(0).get("defaultUserMessage"));
}
@Test
public void testCreateAndReadLoanProductWithAdvancedPaymentAndInterestPaymentWaiverTransaction() {
// given
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
AdvancedPaymentData interestPaymentWaiverAllocation = createInterestPaymentWaiverAllocation();
// when
Integer loanProductId = LOAN_TRANSACTION_HELPER
.getLoanProductId(createLoanJSON(defaultAllocation, interestPaymentWaiverAllocation));
Assertions.assertNotNull(loanProductId);
GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
// then
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
Assertions.assertEquals(2, loanProduct.getPaymentAllocation().size());
Optional<AdvancedPaymentData> first = loanProduct.getPaymentAllocation().stream()
.filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst();
Assertions.assertTrue(first.isPresent());
Assertions.assertEquals(defaultAllocation, first.get());
Optional<AdvancedPaymentData> second = loanProduct.getPaymentAllocation().stream()
.filter(advancedPaymentData -> "INTEREST_PAYMENT_WAIVER".equals(advancedPaymentData.getTransactionType())).findFirst();
Assertions.assertTrue(second.isPresent());
Assertions.assertEquals(interestPaymentWaiverAllocation, second.get());
}
@Test
public void testUpdateLoanProductInterestPaymentWaiverAllocationIsAdded() {
// given a loan with one allocation
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
AdvancedPaymentData interestPaymentWaiverAllocation = createInterestPaymentWaiverAllocation();
Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation));
Assertions.assertNotNull(loanProductId);
GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
Assertions.assertEquals(1, loanProduct.getPaymentAllocation().size());
// when a new allocation is added
LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(),
updateLoanProductRequest(defaultAllocation, interestPaymentWaiverAllocation));
// then it shall be added.
loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
Assertions.assertEquals(2, loanProduct.getPaymentAllocation().size());
Optional<AdvancedPaymentData> first = loanProduct.getPaymentAllocation().stream()
.filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst();
Assertions.assertTrue(first.isPresent());
Assertions.assertEquals(defaultAllocation, first.get());
Optional<AdvancedPaymentData> second = loanProduct.getPaymentAllocation().stream()
.filter(advancedPaymentData -> "INTEREST_PAYMENT_WAIVER".equals(advancedPaymentData.getTransactionType())).findFirst();
Assertions.assertTrue(second.isPresent());
Assertions.assertEquals(interestPaymentWaiverAllocation, second.get());
}
private String createLoanJSON(AdvancedPaymentData... advancedPaymentData) {
return new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4").withRepaymentAfterEvery("1")
.withRepaymentTypeAsMonth().withinterestRatePerPeriod("1")
@@ -267,7 +323,20 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests {
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_INTEREST,
PaymentAllocationType.DUE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
}
private AdvancedPaymentData createInterestPaymentWaiverAllocation() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("INTEREST_PAYMENT_WAIVER");
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_FEE,
PaymentAllocationType.PAST_DUE_PENALTY, PaymentAllocationType.PAST_DUE_INTEREST, PaymentAllocationType.PAST_DUE_PRINCIPAL,
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_INTEREST,
PaymentAllocationType.DUE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
}
@@ -917,6 +917,23 @@ public final class BatchHelper {
return createTransactionRequest(requestId, reference, "goodwillCredit", amount, LocalDate.now(Utils.getZoneIdOfTenant()));
}
/**
* Creates and returns a {@link CreateTransactionLoanCommandStrategy} request with given request ID for payment
* waiver transaction.
*
*
* @param requestId
* the request ID
* @param reference
* the reference
* @param amount
* the amount
* @return BatchRequest the created {@link BatchRequest}
*/
public static BatchRequest interestPaymentWaiverRequest(final Long requestId, final Long reference, final String amount) {
return createTransactionRequest(requestId, reference, "interestPaymentWaiver", amount, LocalDate.now(Utils.getZoneIdOfTenant()));
}
/**
* Creates and returns a {@link CreateTransactionLoanCommandStrategy} request with given request ID for merchant
* issued refund transaction.
@@ -540,6 +540,16 @@ public class ExternalEventConfigurationHelper {
loanUndoReAmortizeBusinessEvent.put("enabled", false);
defaults.add(loanUndoReAmortizeBusinessEvent);
Map<String, Object> loanTransactionInterestPaymentWaiverPostBusinessEvent = new HashMap<>();
loanTransactionInterestPaymentWaiverPostBusinessEvent.put("type", "LoanTransactionInterestPaymentWaiverPostBusinessEvent");
loanTransactionInterestPaymentWaiverPostBusinessEvent.put("enabled", false);
defaults.add(loanTransactionInterestPaymentWaiverPostBusinessEvent);
Map<String, Object> loanTransactionInterestPaymentWaiverPreBusinessEvent = new HashMap<>();
loanTransactionInterestPaymentWaiverPreBusinessEvent.put("type", "LoanTransactionInterestPaymentWaiverPreBusinessEvent");
loanTransactionInterestPaymentWaiverPreBusinessEvent.put("enabled", false);
defaults.add(loanTransactionInterestPaymentWaiverPreBusinessEvent);
return defaults;
}
@@ -601,6 +601,16 @@ public class LoanTransactionHelper extends IntegrationTest {
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "repayment"));
}
public PostLoansLoanIdTransactionsResponse makeInterestPaymentWaiver(final Long loanId,
final PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "interestPaymentWaiver"));
}
public PostLoansLoanIdTransactionsResponse makeInterestPaymentWaiver(final String loanExternalId,
final PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "interestPaymentWaiver"));
}
public PostLoansLoanIdTransactionsResponse reAge(final Long loanId, final PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "reAge"));
}