FINERACT-2208: Penalties are not recalculated after backdated transactions
This commit is contained in:
committed by
Arnold Gálovics
parent
91f0662998
commit
3198b1b99e
+5
@@ -79,6 +79,7 @@ public class LoanChargesApiResource {
|
||||
public static final String COMMAND_PAY = "pay";
|
||||
public static final String COMMAND_WAIVE = "waive";
|
||||
public static final String COMMAND_ADJUSTMENT = "adjustment";
|
||||
public static final String COMMAND_DEACTIVATE_OVERDUE = "deactivateOverdue";
|
||||
private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(
|
||||
Arrays.asList("id", "chargeId", "name", "penalty", "chargeTimeType", "dueAsOfDate", "chargeCalculationType", "percentage",
|
||||
"amountPercentageAppliedTo", "currency", "amountWaived", "amountWrittenOff", "amountOutstanding", "amountOrPercentage",
|
||||
@@ -463,6 +464,10 @@ public class LoanChargesApiResource {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().payLoanCharge(resolvedLoanId, null)
|
||||
.withJson(apiRequestBodyAsJson).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else if (CommandParameterUtil.is(commandParam, COMMAND_DEACTIVATE_OVERDUE)) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().deactivateOverdueLoanCharges(resolvedLoanId, null)
|
||||
.withJson(apiRequestBodyAsJson).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().createLoanCharge(resolvedLoanId)
|
||||
.withJson(apiRequestBodyAsJson).build();
|
||||
|
||||
+4
-1
@@ -294,6 +294,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
||||
|| !loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LoanInterestRecalculationDetails recalculationDetails = loan.getLoanInterestRecalculationDetails();
|
||||
if (recalculationDetails != null && recalculationDetails.isCompoundingToBePostedAsTransaction()) {
|
||||
return;
|
||||
@@ -523,7 +524,9 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
||||
loanCharges = loan.getLoanCharges(lc -> !lc.isDueAtDisbursement() && (lc.isInstalmentFee() ? !DateUtils.isBefore(tillDate, dueDate)
|
||||
: isChargeDue(lc, tillDate, chargeOnDueDate, installment, period.isFirstPeriod())));
|
||||
for (LoanCharge loanCharge : loanCharges) {
|
||||
addChargeAccrual(loanCharge, tillDate, chargeOnDueDate, installment, accrualPeriods);
|
||||
if (loanCharge.isActive()) {
|
||||
addChargeAccrual(loanCharge, tillDate, chargeOnDueDate, installment, accrualPeriods);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+49
@@ -121,6 +121,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeAdjustmentException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeDeactivationException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
|
||||
@@ -130,6 +131,8 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDa
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentParameter;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO;
|
||||
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
|
||||
import org.apache.fineract.portfolio.note.domain.Note;
|
||||
@@ -174,6 +177,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
private final LoanScheduleService loanScheduleService;
|
||||
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||
private final LoanAccountService loanAccountService;
|
||||
private final LoanAdjustmentService loanAdjustmentService;
|
||||
|
||||
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
|
||||
return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate());
|
||||
@@ -776,6 +780,49 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
.build();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult deactivateOverdueLoanCharge(Long loanId, JsonCommand command) {
|
||||
LocalDate fromDueDate = command.dateValueOfParameterNamed("dueDate");
|
||||
|
||||
List<LoanCharge> loanCharges = loanChargeRepository.findByLoanIdAndFromDueDate(loanId, fromDueDate);
|
||||
loanCharges.forEach(this::inactivateOverdueLoanCharge);
|
||||
|
||||
Loan loan = loanAssembler.assembleFrom(loanId);
|
||||
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = loan
|
||||
.getRepaymentScheduleInstallments(si -> DateUtils.isDateInRangeInclusive(fromDueDate, si.getFromDate(), si.getDueDate())
|
||||
|| DateUtils.isAfter(si.getFromDate(), fromDueDate));
|
||||
repaymentScheduleInstallments.forEach(si -> si.setPenaltyAccrued(null));
|
||||
List<LoanTransaction> accrualsToReverse = loan.getLoanTransactions(
|
||||
tx -> tx.isNotReversed() && DateUtils.isAfterInclusive(tx.getTransactionDate(), fromDueDate) && tx.isAccrualRelated());
|
||||
accrualsToReverse.forEach(tx -> loanAdjustmentService.adjustLoanTransaction(loan, tx,
|
||||
LoanAdjustmentParameter.builder().transactionDate(tx.getTransactionDate()).build(), null, new HashMap<>()));
|
||||
|
||||
loanRepositoryWrapper.saveAndFlush(loan);
|
||||
|
||||
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
|
||||
return commandProcessingResultBuilder.withLoanId(loanId) //
|
||||
.withEntityId(loanId) //
|
||||
.withEntityExternalId(loan.getExternalId()) //
|
||||
.build();
|
||||
}
|
||||
|
||||
private void inactivateOverdueLoanCharge(LoanCharge loanCharge) {
|
||||
if (!loanCharge.getChargeTimeType().isOverdueInstallment()) {
|
||||
throw new LoanChargeDeactivationException("loan.charge.deactivate.invalid.charge.type",
|
||||
"Loan charge is not an overdue installment charge");
|
||||
}
|
||||
|
||||
if (!loanCharge.isActive()) {
|
||||
throw new LoanChargeDeactivationException("loan.charge.deactivate.invalid.status", "Loan charge is not active");
|
||||
}
|
||||
|
||||
loanCharge.setActive(false);
|
||||
loanChargeRepository.saveAndFlush(loanCharge);
|
||||
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateChargeBusinessEvent(loanCharge));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void applyOverdueChargesForLoan(final Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList) {
|
||||
@@ -802,8 +849,10 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(overdueInstallment.toString());
|
||||
final JsonCommand command = JsonCommand.from(overdueInstallment.toString(), parsedCommand, this.fromApiJsonHelper, null, null,
|
||||
null, null, null, loanId, null, null, null, null, null, null, null, null);
|
||||
|
||||
LoanOverdueDTO overdueDTO = applyChargeToOverdueLoanInstallment(loan, overdueInstallment.getChargeId(),
|
||||
overdueInstallment.getPeriodNumber(), command);
|
||||
|
||||
loan = overdueDTO.getLoan();
|
||||
runInterestRecalculation = runInterestRecalculation || overdueDTO.isRunInterestRecalculation();
|
||||
if (DateUtils.isAfter(recalculateFrom, overdueDTO.getRecalculateFrom())) {
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LoanJournalEntryPoster {
|
||||
|
||||
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
|
||||
|
||||
public void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds) {
|
||||
|
||||
final MonetaryCurrency currency = loan.getCurrency();
|
||||
boolean isAccountTransfer = false;
|
||||
List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
|
||||
if (loan.isChargedOff()) {
|
||||
accountingBridgeData = loan.deriveAccountingBridgeDataForChargeOff(currency.getCode(), existingTransactionIds,
|
||||
existingReversedTransactionIds, isAccountTransfer);
|
||||
} else {
|
||||
accountingBridgeData.add(loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
|
||||
existingReversedTransactionIds, isAccountTransfer));
|
||||
}
|
||||
for (Map<String, Object> accountingData : accountingBridgeData) {
|
||||
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingData);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+26
-208
@@ -195,7 +195,6 @@ import org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbur
|
||||
import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
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;
|
||||
@@ -204,6 +203,8 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTr
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanOfficerValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentParameter;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
|
||||
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
|
||||
@@ -278,6 +279,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
private final LoanOfficerService loanOfficerService;
|
||||
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||
private final LoanAccountService loanAccountService;
|
||||
private final LoanJournalEntryPoster journalEntryPoster;
|
||||
private final LoanAdjustmentService loanAdjustmentService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -534,7 +537,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction));
|
||||
}
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
@@ -816,7 +819,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
if (!changes.isEmpty()) {
|
||||
createNote(loan, command, changes);
|
||||
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
}
|
||||
final Set<LoanCharge> loanCharges = loan.getActiveCharges();
|
||||
@@ -1165,7 +1168,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
|
||||
@@ -1332,6 +1335,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
"Interest refund transaction: " + transactionId + " cannot be reversed or adjusted directly", transactionId);
|
||||
}
|
||||
|
||||
Long commandId = command.commandId();
|
||||
final String noteText = command.stringValueOfParameterNamed("note");
|
||||
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
|
||||
@@ -1354,181 +1359,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
changes.put("dateFormat", command.dateFormat());
|
||||
changes.put("paymentTypeId", command.longValueOfParameterNamed("paymentTypeId"));
|
||||
|
||||
final List<Long> existingTransactionIds = new ArrayList<>();
|
||||
final List<Long> existingReversedTransactionIds = new ArrayList<>();
|
||||
|
||||
final Money transactionAmountAsMoney = Money.of(loan.getCurrency(), transactionAmount);
|
||||
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createPaymentDetail(command, changes);
|
||||
LoanTransaction newTransactionDetail = LoanTransaction.repaymentType(transactionToAdjust.getTypeOf(), loan.getOffice(),
|
||||
transactionAmountAsMoney, paymentDetail, transactionDate, txnExternalId, transactionToAdjust.getChargeRefundChargeType());
|
||||
if (transactionToAdjust.isInterestWaiver()) {
|
||||
Money unrecognizedIncome = transactionAmountAsMoney.zero();
|
||||
Money interestComponent = transactionAmountAsMoney;
|
||||
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
|
||||
Money receivableInterest = loan.getReceivableInterest(transactionDate);
|
||||
if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) {
|
||||
interestComponent = receivableInterest;
|
||||
unrecognizedIncome = transactionAmountAsMoney.minus(receivableInterest);
|
||||
}
|
||||
}
|
||||
newTransactionDetail = LoanTransaction.waiver(loan.getOffice(), loan, transactionAmountAsMoney, transactionDate,
|
||||
interestComponent, unrecognizedIncome, txnExternalId);
|
||||
}
|
||||
|
||||
LocalDate recalculateFrom = null;
|
||||
LoanAdjustmentParameter parameter = LoanAdjustmentParameter.builder().transactionAmount(transactionAmount)
|
||||
.paymentDetail(paymentDetail).transactionDate(transactionDate).txnExternalId(txnExternalId)
|
||||
.reversalTxnExternalId(reversalTxnExternalId).noteText(noteText).build();
|
||||
|
||||
if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
recalculateFrom = DateUtils.isAfter(transactionToAdjust.getTransactionDate(), transactionDate) ? transactionDate
|
||||
: transactionToAdjust.getTransactionDate();
|
||||
}
|
||||
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
|
||||
HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO();
|
||||
if (loan.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE)) {
|
||||
// validate cumulative
|
||||
loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, transactionToAdjust.getTransactionDate(),
|
||||
LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
|
||||
}
|
||||
// common validations
|
||||
loanTransactionValidator.validateRepaymentDateIsOnHoliday(newTransactionDetail.getTransactionDate(),
|
||||
holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
|
||||
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newTransactionDetail.getTransactionDate(),
|
||||
holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
|
||||
|
||||
adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionToAdjust, existingTransactionIds,
|
||||
existingReversedTransactionIds, scheduleGeneratorDTO, reversalTxnExternalId);
|
||||
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
|
||||
}
|
||||
|
||||
boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero();
|
||||
if (thereIsNewTransaction) {
|
||||
if (paymentDetail != null) {
|
||||
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
|
||||
}
|
||||
this.loanTransactionRepository.saveAndFlush(newTransactionDetail);
|
||||
}
|
||||
|
||||
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||
|
||||
final String noteText = command.stringValueOfParameterNamed("note");
|
||||
if (StringUtils.isNotBlank(noteText)) {
|
||||
changes.put("note", noteText);
|
||||
Note note;
|
||||
/**
|
||||
* If a new transaction is not created, associate note with the transaction to be adjusted
|
||||
**/
|
||||
if (thereIsNewTransaction) {
|
||||
note = Note.loanTransactionNote(loan, newTransactionDetail, noteText);
|
||||
} else {
|
||||
note = Note.loanTransactionNote(loan, transactionToAdjust, noteText);
|
||||
}
|
||||
this.noteRepository.save(note);
|
||||
}
|
||||
|
||||
Collection<Long> transactionIds = new ArrayList<>();
|
||||
List<LoanTransaction> transactions = loan.getLoanTransactions();
|
||||
for (LoanTransaction transaction : transactions) {
|
||||
if (transaction.isRefund() && transaction.isNotReversed()) {
|
||||
transactionIds.add(transaction.getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (!transactionIds.isEmpty()) {
|
||||
this.accountTransfersWritePlatformService.reverseTransfersWithFromAccountTransactions(transactionIds,
|
||||
PortfolioAccountType.LOAN);
|
||||
loan.updateLoanSummaryAndStatus();
|
||||
}
|
||||
|
||||
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, loan.isInterestBearingAndInterestRecalculationEnabled(),
|
||||
false);
|
||||
|
||||
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
|
||||
|
||||
LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust);
|
||||
if (newTransactionDetail.isRepaymentLikeType() && thereIsNewTransaction) {
|
||||
eventData.setNewTransactionDetail(newTransactionDetail);
|
||||
}
|
||||
Long entityId = transactionToAdjust.getId();
|
||||
ExternalId entityExternalId = transactionToAdjust.getExternalId();
|
||||
|
||||
if (thereIsNewTransaction) {
|
||||
entityId = newTransactionDetail.getId();
|
||||
entityExternalId = newTransactionDetail.getExternalId();
|
||||
}
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData));
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
.withEntityId(entityId) //
|
||||
.withEntityExternalId(entityExternalId) //
|
||||
.withOfficeId(loan.getOfficeId()) //
|
||||
.withClientId(loan.getClientId()) //
|
||||
.withGroupId(loan.getGroupId()) //
|
||||
.withLoanId(loanId) //
|
||||
.with(changes).build();
|
||||
}
|
||||
|
||||
public void adjustExistingTransaction(final Loan loan, final LoanTransaction newTransactionDetail,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction transactionForAdjustment,
|
||||
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO, final ExternalId reversalExternalId) {
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
|
||||
transactionForAdjustment.getTransactionDate());
|
||||
|
||||
if (transactionForAdjustment.isNotRepaymentLikeType() && transactionForAdjustment.isNotWaiver()
|
||||
&& transactionForAdjustment.isNotCreditBalanceRefund()) {
|
||||
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver or credit balance refund can be adjusted.";
|
||||
throw new InvalidLoanTransactionTypeException("transaction",
|
||||
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage);
|
||||
}
|
||||
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(transactionForAdjustment.getLoan(),
|
||||
transactionForAdjustment, "reversed");
|
||||
transactionForAdjustment.reverse(reversalExternalId);
|
||||
transactionForAdjustment.manuallyAdjustedOrReversed();
|
||||
|
||||
if (transactionForAdjustment.getTypeOf().equals(LoanTransactionType.MERCHANT_ISSUED_REFUND)
|
||||
|| transactionForAdjustment.getTypeOf().equals(LoanTransactionType.PAYOUT_REFUND)) {
|
||||
loan.getLoanTransactions().stream() //
|
||||
.filter(LoanTransaction::isNotReversed)
|
||||
.filter(loanTransaction -> loanTransaction.getLoanTransactionRelations().stream()
|
||||
.anyMatch(relation -> relation.getRelationType().equals(LoanTransactionRelationTypeEnum.RELATED)
|
||||
&& relation.getToTransaction().getId().equals(transactionForAdjustment.getId())))
|
||||
.forEach(loanTransaction -> {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(),
|
||||
loanTransaction, "reversed");
|
||||
loanTransaction.reverse();
|
||||
loanTransaction.manuallyAdjustedOrReversed();
|
||||
LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData));
|
||||
});
|
||||
}
|
||||
|
||||
if (loan.isClosedWrittenOff()) {
|
||||
// find write off transaction and reverse it
|
||||
final LoanTransaction writeOffTransaction = loan.findWriteOffTransaction();
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(writeOffTransaction.getLoan(), writeOffTransaction,
|
||||
"reversed");
|
||||
writeOffTransaction.reverse();
|
||||
}
|
||||
|
||||
loan.updateLoanSummaryAndStatus();
|
||||
|
||||
if (newTransactionDetail.isRepaymentLikeType() || newTransactionDetail.isInterestWaiver()) {
|
||||
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, newTransactionDetail,
|
||||
loanLifecycleStateMachine, transactionForAdjustment, scheduleGeneratorDTO);
|
||||
}
|
||||
return loanAdjustmentService.adjustLoanTransaction(loan, transactionToAdjust, parameter, commandId, changes);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -1611,7 +1448,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
this.noteRepository.save(note);
|
||||
}
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
this.loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargebackTransactionBusinessEvent(newTransaction));
|
||||
@@ -1714,7 +1551,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction));
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
@@ -1808,7 +1645,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(loanTransaction));
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
|
||||
builder.withEntityId(loanTransaction.getId()).withEntityExternalId(loanTransaction.getExternalId());
|
||||
@@ -1885,7 +1722,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
// disable all active standing instructions linked to the loan
|
||||
this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan);
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
|
||||
CommandProcessingResult result;
|
||||
@@ -2021,7 +1858,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
this.loanTransactionRepository.saveAndFlush(newTransferTransaction);
|
||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanInitiateTransferBusinessEvent(loan));
|
||||
return newTransferTransaction;
|
||||
}
|
||||
@@ -2051,7 +1888,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
|
||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAcceptTransferBusinessEvent(loan));
|
||||
|
||||
return newTransferAcceptanceTransaction;
|
||||
@@ -2077,7 +1914,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
|
||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanWithdrawTransferBusinessEvent(loan));
|
||||
|
||||
return newTransferAcceptanceTransaction;
|
||||
@@ -2209,25 +2046,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
.build();
|
||||
}
|
||||
|
||||
private void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds) {
|
||||
|
||||
final MonetaryCurrency currency = loan.getCurrency();
|
||||
boolean isAccountTransfer = false;
|
||||
List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
|
||||
if (loan.isChargedOff()) {
|
||||
accountingBridgeData = loan.deriveAccountingBridgeDataForChargeOff(currency.getCode(), existingTransactionIds,
|
||||
existingReversedTransactionIds, isAccountTransfer);
|
||||
} else {
|
||||
accountingBridgeData.add(loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
|
||||
existingReversedTransactionIds, isAccountTransfer));
|
||||
}
|
||||
for (Map<String, Object> accountingData : accountingBridgeData) {
|
||||
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void applyMeetingDateChanges(final Calendar calendar, final Collection<CalendarInstance> loanCalendarInstances) {
|
||||
@@ -2503,7 +2321,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
|
||||
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
@@ -2647,7 +2465,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
false);
|
||||
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
@@ -2709,7 +2527,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
false);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan));
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
return loan;
|
||||
}
|
||||
@@ -3129,7 +2947,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
|
||||
});
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeOffPostBusinessEvent(chargeOffTransaction));
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(command.commandId()) //
|
||||
@@ -3192,7 +3010,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
reprocessLoanTransactionsService.reprocessTransactions(loan);
|
||||
|
||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoChargeOffBusinessEvent(chargedOffTransaction));
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
@@ -3267,7 +3085,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||
|
||||
// Create journal entries for the new transaction(s)
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
|
||||
Long entityId = refundTransaction.getId();
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.adjustment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class LoanAdjustmentParameter {
|
||||
|
||||
private BigDecimal transactionAmount;
|
||||
private PaymentDetail paymentDetail;
|
||||
private LocalDate transactionDate;
|
||||
private ExternalId txnExternalId;
|
||||
private ExternalId reversalTxnExternalId;
|
||||
private String noteText;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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.adjustment;
|
||||
|
||||
import java.util.Map;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
|
||||
public interface LoanAdjustmentService {
|
||||
|
||||
CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter,
|
||||
Long commandId, Map<String, Object> changes);
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* 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.adjustment;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.account.PortfolioAccountType;
|
||||
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||
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.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
|
||||
|
||||
private final LoanTransactionValidator loanTransactionValidator;
|
||||
private final LoanRepositoryWrapper loanRepositoryWrapper;
|
||||
private final LoanAccountDomainService loanAccountDomainService;
|
||||
private final NoteRepository noteRepository;
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
|
||||
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanUtilService loanUtilService;
|
||||
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
|
||||
private final LoanLifecycleStateMachine loanLifecycleStateMachine;
|
||||
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
|
||||
private final LoanDownPaymentHandlerService loanDownPaymentHandlerService;
|
||||
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
private final LoanJournalEntryPoster journalEntryPoster;
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter,
|
||||
Long commandId, Map<String, Object> changes) {
|
||||
LocalDate transactionDate = parameter.getTransactionDate();
|
||||
BigDecimal transactionAmount = parameter.getTransactionAmount();
|
||||
PaymentDetail paymentDetail = parameter.getPaymentDetail();
|
||||
ExternalId txnExternalId = parameter.getTxnExternalId();
|
||||
ExternalId reversalTxnExternalId = parameter.getReversalTxnExternalId();
|
||||
String noteText = parameter.getNoteText();
|
||||
|
||||
final List<Long> existingTransactionIds = new ArrayList<>();
|
||||
final List<Long> existingReversedTransactionIds = new ArrayList<>();
|
||||
|
||||
final Money transactionAmountAsMoney = Money.of(loan.getCurrency(), transactionAmount);
|
||||
LoanTransaction newTransactionDetail = LoanTransaction.repaymentType(transactionToAdjust.getTypeOf(), loan.getOffice(),
|
||||
transactionAmountAsMoney, paymentDetail, transactionDate, txnExternalId, transactionToAdjust.getChargeRefundChargeType());
|
||||
if (transactionToAdjust.isInterestWaiver()) {
|
||||
Money unrecognizedIncome = transactionAmountAsMoney.zero();
|
||||
Money interestComponent = transactionAmountAsMoney;
|
||||
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
|
||||
Money receivableInterest = loan.getReceivableInterest(transactionDate);
|
||||
if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) {
|
||||
interestComponent = receivableInterest;
|
||||
unrecognizedIncome = transactionAmountAsMoney.minus(receivableInterest);
|
||||
}
|
||||
}
|
||||
newTransactionDetail = LoanTransaction.waiver(loan.getOffice(), loan, transactionAmountAsMoney, transactionDate,
|
||||
interestComponent, unrecognizedIncome, txnExternalId);
|
||||
}
|
||||
|
||||
LocalDate recalculateFrom = null;
|
||||
|
||||
if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
recalculateFrom = DateUtils.isAfter(transactionToAdjust.getTransactionDate(), transactionDate) ? transactionDate
|
||||
: transactionToAdjust.getTransactionDate();
|
||||
}
|
||||
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
|
||||
|
||||
HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO();
|
||||
if (loan.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE)) {
|
||||
// validate cumulative
|
||||
loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, transactionToAdjust.getTransactionDate(),
|
||||
LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
|
||||
}
|
||||
// common validations
|
||||
loanTransactionValidator.validateRepaymentDateIsOnHoliday(newTransactionDetail.getTransactionDate(),
|
||||
holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
|
||||
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newTransactionDetail.getTransactionDate(),
|
||||
holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
|
||||
|
||||
adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionToAdjust, existingTransactionIds,
|
||||
existingReversedTransactionIds, scheduleGeneratorDTO, reversalTxnExternalId);
|
||||
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
|
||||
}
|
||||
|
||||
boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero();
|
||||
if (thereIsNewTransaction) {
|
||||
if (paymentDetail != null) {
|
||||
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
|
||||
}
|
||||
this.loanTransactionRepository.saveAndFlush(newTransactionDetail);
|
||||
}
|
||||
|
||||
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||
|
||||
if (StringUtils.isNotBlank(noteText)) {
|
||||
changes.put("note", noteText);
|
||||
Note note;
|
||||
/**
|
||||
* If a new transaction is not created, associate note with the transaction to be adjusted
|
||||
**/
|
||||
if (thereIsNewTransaction) {
|
||||
note = Note.loanTransactionNote(loan, newTransactionDetail, noteText);
|
||||
} else {
|
||||
note = Note.loanTransactionNote(loan, transactionToAdjust, noteText);
|
||||
}
|
||||
this.noteRepository.save(note);
|
||||
}
|
||||
|
||||
Collection<Long> transactionIds = new ArrayList<>();
|
||||
List<LoanTransaction> transactions = loan.getLoanTransactions();
|
||||
for (LoanTransaction transaction : transactions) {
|
||||
if (transaction.isRefund() && transaction.isNotReversed()) {
|
||||
transactionIds.add(transaction.getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (!transactionIds.isEmpty()) {
|
||||
this.accountTransfersWritePlatformService.reverseTransfersWithFromAccountTransactions(transactionIds,
|
||||
PortfolioAccountType.LOAN);
|
||||
loan.updateLoanSummaryAndStatus();
|
||||
}
|
||||
|
||||
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, loan.isInterestBearingAndInterestRecalculationEnabled(),
|
||||
false);
|
||||
|
||||
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
|
||||
|
||||
LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust);
|
||||
if (newTransactionDetail.isRepaymentLikeType() && thereIsNewTransaction) {
|
||||
eventData.setNewTransactionDetail(newTransactionDetail);
|
||||
}
|
||||
Long entityId = transactionToAdjust.getId();
|
||||
ExternalId entityExternalId = transactionToAdjust.getExternalId();
|
||||
|
||||
if (thereIsNewTransaction) {
|
||||
entityId = newTransactionDetail.getId();
|
||||
entityExternalId = newTransactionDetail.getExternalId();
|
||||
}
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData));
|
||||
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
|
||||
return new CommandProcessingResultBuilder() //
|
||||
.withCommandId(commandId) //
|
||||
.withEntityId(entityId) //
|
||||
.withEntityExternalId(entityExternalId) //
|
||||
.withOfficeId(loan.getOfficeId()) //
|
||||
.withClientId(loan.getClientId()) //
|
||||
.withGroupId(loan.getGroupId()) //
|
||||
.withLoanId(loan.getId()) //
|
||||
.with(changes).build();
|
||||
}
|
||||
|
||||
public void adjustExistingTransaction(final Loan loan, final LoanTransaction newTransactionDetail,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction transactionForAdjustment,
|
||||
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO, final ExternalId reversalExternalId) {
|
||||
existingTransactionIds.addAll(loan.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
|
||||
|
||||
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
|
||||
transactionForAdjustment.getTransactionDate());
|
||||
|
||||
if (!transactionForAdjustment.isAccrualRelated() && transactionForAdjustment.isNotRepaymentLikeType()
|
||||
&& transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()) {
|
||||
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, accrual or credit balance refund can be adjusted.";
|
||||
throw new InvalidLoanTransactionTypeException("transaction",
|
||||
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage);
|
||||
}
|
||||
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(transactionForAdjustment.getLoan(),
|
||||
transactionForAdjustment, "reversed");
|
||||
transactionForAdjustment.reverse(reversalExternalId);
|
||||
transactionForAdjustment.manuallyAdjustedOrReversed();
|
||||
|
||||
if (transactionForAdjustment.getTypeOf().equals(LoanTransactionType.MERCHANT_ISSUED_REFUND)
|
||||
|| transactionForAdjustment.getTypeOf().equals(LoanTransactionType.PAYOUT_REFUND)) {
|
||||
loan.getLoanTransactions().stream() //
|
||||
.filter(LoanTransaction::isNotReversed)
|
||||
.filter(loanTransaction -> loanTransaction.getLoanTransactionRelations().stream()
|
||||
.anyMatch(relation -> relation.getRelationType().equals(LoanTransactionRelationTypeEnum.RELATED)
|
||||
&& relation.getToTransaction().getId().equals(transactionForAdjustment.getId())))
|
||||
.forEach(loanTransaction -> {
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(),
|
||||
loanTransaction, "reversed");
|
||||
loanTransaction.reverse();
|
||||
loanTransaction.manuallyAdjustedOrReversed();
|
||||
LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData));
|
||||
});
|
||||
}
|
||||
|
||||
if (loan.isClosedWrittenOff()) {
|
||||
// find write off transaction and reverse it
|
||||
final LoanTransaction writeOffTransaction = loan.findWriteOffTransaction();
|
||||
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(writeOffTransaction.getLoan(), writeOffTransaction,
|
||||
"reversed");
|
||||
writeOffTransaction.reverse();
|
||||
}
|
||||
|
||||
loan.updateLoanSummaryAndStatus();
|
||||
|
||||
if (newTransactionDetail.isRepaymentLikeType() || newTransactionDetail.isInterestWaiver()) {
|
||||
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, newTransactionDetail,
|
||||
loanLifecycleStateMachine, transactionForAdjustment, scheduleGeneratorDTO);
|
||||
}
|
||||
}
|
||||
|
||||
private Loan saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
|
||||
/*
|
||||
* Due to the "saveAndFlushLoanWithDataIntegrityViolationChecks" method the loan is saved and flushed in the
|
||||
* middle of the transaction. EclipseLink is in some situations are saving inconsistently the newly created
|
||||
* associations, like the newly created repayment schedule installments. The save and flush cannot be removed
|
||||
* safely till any native queries are used as part of this transaction either. See:
|
||||
* this.loanAccountDomainService.recalculateAccruals(loan);
|
||||
*/
|
||||
try {
|
||||
loanRepaymentScheduleInstallmentRepository.saveAll(loan.getRepaymentScheduleInstallments());
|
||||
return this.loanRepositoryWrapper.saveAndFlush(loan);
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException e) {
|
||||
final Throwable realCause = e.getCause();
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
|
||||
if (realCause.getMessage().toLowerCase().contains("external_id_unique")) {
|
||||
baseDataValidator.reset().parameter(LoanApiConstants.externalIdParameterName).failWithCode("value.must.be.unique");
|
||||
}
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors, e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -127,6 +127,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetails
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl;
|
||||
@@ -142,6 +143,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServic
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
|
||||
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
|
||||
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
|
||||
@@ -296,7 +298,7 @@ public class LoanAccountConfiguration {
|
||||
LoanAccrualsProcessingService loanAccrualsProcessingService,
|
||||
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanChargeValidator loanChargeValidator,
|
||||
LoanScheduleService loanScheduleService, ReprocessLoanTransactionsService reprocessLoanTransactionsService,
|
||||
LoanAccountService loanAccountService) {
|
||||
LoanAccountService loanAccountService, LoanAdjustmentService loanAdjustmentService) {
|
||||
return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository,
|
||||
businessEventNotifierService, loanTransactionRepository, accountTransfersWritePlatformService, loanRepositoryWrapper,
|
||||
journalEntryWritePlatformService, loanAccountDomainService, loanChargeRepository, loanWritePlatformService, loanUtilService,
|
||||
@@ -304,7 +306,7 @@ public class LoanAccountConfiguration {
|
||||
configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory,
|
||||
accountTransferDetailRepository, loanChargeAssembler, paymentDetailWritePlatformService, noteRepository,
|
||||
loanAccrualTransactionBusinessEventService, loanAccrualsProcessingService, loanDownPaymentTransactionValidator,
|
||||
loanChargeValidator, loanScheduleService, reprocessLoanTransactionsService, loanAccountService);
|
||||
loanChargeValidator, loanScheduleService, reprocessLoanTransactionsService, loanAccountService, loanAdjustmentService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -392,7 +394,8 @@ public class LoanAccountConfiguration {
|
||||
LoanOfficerValidator loanOfficerValidator, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator,
|
||||
LoanDisbursementService loanDisbursementService, LoanScheduleService loanScheduleService,
|
||||
LoanChargeValidator loanChargeValidator, LoanOfficerService loanOfficerService,
|
||||
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanAccountService loanAccountService) {
|
||||
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanAccountService loanAccountService,
|
||||
LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService) {
|
||||
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
|
||||
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
|
||||
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
|
||||
@@ -406,7 +409,7 @@ public class LoanAccountConfiguration {
|
||||
loanAccountLockService, externalIdFactory, loanAccrualTransactionBusinessEventService, errorHandler,
|
||||
loanDownPaymentHandlerService, loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator,
|
||||
loanDownPaymentTransactionValidator, loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService,
|
||||
reprocessLoanTransactionsService, loanAccountService);
|
||||
reprocessLoanTransactionsService, loanAccountService, journalEntryPoster, loanAdjustmentService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
+5
-4
@@ -78,6 +78,7 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationVa
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentServiceImpl;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository;
|
||||
@@ -90,10 +91,10 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LoanWritePlatformServiceJpaRepositoryImplTest {
|
||||
class LoanAdjustmentServiceImplTest {
|
||||
|
||||
@InjectMocks
|
||||
private LoanWritePlatformServiceJpaRepositoryImpl loanWritePlatformService;
|
||||
private LoanAdjustmentServiceImpl underTest;
|
||||
|
||||
@Mock
|
||||
private LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
|
||||
@@ -240,7 +241,7 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
|
||||
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
|
||||
|
||||
// Act
|
||||
loanWritePlatformService.adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
|
||||
underTest.adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
|
||||
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalExternalId);
|
||||
|
||||
// Assert
|
||||
@@ -285,7 +286,7 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
|
||||
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
|
||||
|
||||
// Act
|
||||
loanWritePlatformService.adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
|
||||
underTest.adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
|
||||
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalExternalId);
|
||||
|
||||
// Assert
|
||||
Reference in New Issue
Block a user