FINERACT-2279: Contract Termination - Undo

This commit is contained in:
mariiaKraievska
2025-06-18 16:19:33 +02:00
committed by Adam Saghy
parent 11cecb0d45
commit ee29ce23ac
23 changed files with 412 additions and 3 deletions
@@ -1234,6 +1234,8 @@ public class LoansApiResource {
commandRequest = builder.assignDelinquency(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.CONTRACT_TERMINATION_COMMAND)) {
commandRequest = builder.applyContractTermination(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.UNDO_CONTRACT_TERMINATION_COMMAND)) {
commandRequest = builder.undoContractTermination(resolvedLoanId).build();
}
if (commandRequest == null) {
@@ -733,6 +733,8 @@ final class LoansApiResourceSwagger {
public boolean capitalizedIncomeAmortization;
@Schema(example = "false")
public boolean capitalizedIncomeAdjustment;
@Schema(example = "false")
public boolean contractTermination;
}
static final class GetLoansLoanIdPaymentDetailData {
@@ -0,0 +1,40 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.handler.loan.contracttermination;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.service.contracttermination.LoanContractTerminationServiceImpl;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@CommandType(entity = "LOAN", action = "CONTRACT_TERMINATION_UNDO")
public class UndoLoanContractTerminationCommandHandler implements NewCommandSourceHandler {
private final LoanContractTerminationServiceImpl loanContractTerminationService;
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return loanContractTerminationService.undoContractTermination(command);
}
}
@@ -1081,4 +1081,12 @@ public final class LoanTransactionValidatorImpl implements LoanTransactionValida
baseDataValidator.reset().parameter("externalId").value(externalId).notExceedingLengthOf(100);
}
}
@Override
public void validateReversalExternalId(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
final String reversalExternalId = this.fromApiJsonHelper.extractStringNamed("reversalExternalId", element);
if (StringUtils.isNotBlank(reversalExternalId)) {
baseDataValidator.reset().parameter("reversalExternalId").value(reversalExternalId).notExceedingLengthOf(100);
}
}
}
@@ -32,19 +32,25 @@ 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.core.service.ExternalIdFactory;
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.domain.loan.transaction.LoanTransactionContractTerminationPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoContractTerminationBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
@@ -65,6 +71,9 @@ public class LoanContractTerminationServiceImpl {
private final ExternalIdFactory externalIdFactory;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTransactionService loanTransactionService;
private final LoanScheduleService loanScheduleService;
private final LoanChargeValidator loanChargeValidator;
private final ProgressiveLoanTransactionValidator loanTransactionValidator;
public CommandProcessingResult applyContractTermination(final JsonCommand command) {
final Loan loan = loanAssembler.assembleFrom(command.getLoanId());
@@ -116,6 +125,67 @@ public class LoanContractTerminationServiceImpl {
.with(changes).build();
}
public CommandProcessingResult undoContractTermination(final JsonCommand command) {
final Long loanId = command.getLoanId();
loanTransactionValidator.validateContractTerminationUndo(command, loanId);
final Loan loan = loanAssembler.assembleFrom(loanId);
final LoanTransaction contractTerminationTransaction = loan.findContractTerminationTransaction();
businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoContractTerminationBusinessEvent(contractTerminationTransaction));
businessEventNotifierService.notifyPreBusinessEvent(
new LoanAdjustTransactionBusinessEvent(new LoanAdjustTransactionBusinessEvent.Data(contractTerminationTransaction)));
// check if reversalExternalId is provided
final String reversalExternalId = command.stringValueOfParameterNamedAllowingNull(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME);
final ExternalId reversalTxnExternalId = ExternalIdFactory.produce(reversalExternalId);
final Map<String, Object> changes = new LinkedHashMap<>();
// Add note if provided
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
final Note note = Note.loanTransactionNote(loan, contractTerminationTransaction, noteText);
noteRepository.save(note);
}
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(contractTerminationTransaction.getLoan(),
contractTerminationTransaction, "reversed");
contractTerminationTransaction.reverse(reversalTxnExternalId);
contractTerminationTransaction.manuallyAdjustedOrReversed();
loan.liftContractTerminationSubStatus();
changes.put(LoanApiConstants.subStatusAttributeName, loan.getLoanSubStatus());
loanTransactionRepository.saveAndFlush(contractTerminationTransaction);
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule()) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
reprocessLoanTransactionsService.reprocessTransactions(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoContractTerminationBusinessEvent(contractTerminationTransaction));
final LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(
contractTerminationTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData));
return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.withEntityId(contractTerminationTransaction.getId()) //
.withEntityExternalId(contractTerminationTransaction.getExternalId()) //
.with(changes) //
.build();
}
public void validateContractTermination(final Loan loan) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
@@ -109,7 +109,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -199,7 +199,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));