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
@@ -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));