FINERACT-2208: Penalties are not recalculated after backdated transactions

This commit is contained in:
Arnold Galovics
2025-03-12 16:52:30 +01:00
committed by Arnold Gálovics
parent 91f0662998
commit 3198b1b99e
19 changed files with 1099 additions and 234 deletions
@@ -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();
@@ -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);
}
}
}
@@ -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())) {
@@ -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);
}
}
}
@@ -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();
@@ -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;
}
@@ -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);
}
@@ -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;
}
}
}
@@ -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
@@ -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