FINERACT-2208: Penalties are not recalculated after backdated transactions
This commit is contained in:
committed by
Arnold Gálovics
parent
91f0662998
commit
3198b1b99e
+9
@@ -799,6 +799,15 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder deactivateOverdueLoanCharges(final Long loanId, final Long loanChargeId) {
|
||||
this.actionName = "DEACTIVATEOVERDUE";
|
||||
this.entityName = "LOANCHARGE";
|
||||
this.entityId = loanChargeId;
|
||||
this.loanId = loanId;
|
||||
this.href = "/loans/" + loanId + "/charges/" + loanChargeId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder deleteLoanCharge(final Long loanId, final Long loanChargeId) {
|
||||
this.actionName = "DELETE";
|
||||
this.entityName = "LOANCHARGE";
|
||||
|
||||
+8
@@ -302,6 +302,10 @@ public final class DateUtils {
|
||||
return isAfterBusinessDate(localDate);
|
||||
}
|
||||
|
||||
public static boolean isDateInThePast(final LocalDate localDate) {
|
||||
return isBeforeBusinessDate(localDate);
|
||||
}
|
||||
|
||||
public static int compare(LocalDate first, LocalDate second) {
|
||||
return compare(first, second, true);
|
||||
}
|
||||
@@ -334,6 +338,10 @@ public final class DateUtils {
|
||||
return first != null && (second == null || first.isAfter(second));
|
||||
}
|
||||
|
||||
public static boolean isAfterInclusive(LocalDate first, LocalDate second) {
|
||||
return isAfter(first, second) || isEqual(first, second);
|
||||
}
|
||||
|
||||
public static long getDifference(LocalDate first, LocalDate second, @NotNull ChronoUnit unit) {
|
||||
if (first == null || second == null) {
|
||||
throw new IllegalArgumentException("Dates must not be null to get difference");
|
||||
|
||||
+9
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
@@ -30,4 +32,11 @@ public interface LoanChargeRepository extends JpaRepository<LoanCharge, Long>, J
|
||||
|
||||
@Query(FIND_ID_BY_EXTERNAL_ID)
|
||||
Long findIdByExternalId(@Param("externalId") ExternalId externalId);
|
||||
|
||||
@Query("""
|
||||
SELECT lc FROM LoanCharge lc
|
||||
WHERE lc.loan.id = :loanId
|
||||
AND lc.dueDate >= :fromDate
|
||||
""")
|
||||
List<LoanCharge> findByLoanIdAndFromDueDate(@Param("loanId") Long loanId, @Param("fromDate") LocalDate fromDate);
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
public class LoanChargeDeactivationException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public LoanChargeDeactivationException(final String errorCode, final String defaultUserMessage,
|
||||
final Object... defaultUserMessageArgs) {
|
||||
super("error.msg." + errorCode, defaultUserMessage, defaultUserMessageArgs);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@CommandType(entity = "LOANCHARGE", action = "DEACTIVATEOVERDUE")
|
||||
public class LoanChargeDeactivateOverdueCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final LoanChargeWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return writePlatformService.deactivateOverdueLoanCharge(command.getLoanId(), command);
|
||||
}
|
||||
}
|
||||
+2
@@ -41,5 +41,7 @@ public interface LoanChargeWritePlatformService {
|
||||
|
||||
CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long loanChargeId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult deactivateOverdueLoanCharge(Long loanId, JsonCommand command);
|
||||
|
||||
void applyOverdueChargesForLoan(Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList);
|
||||
}
|
||||
|
||||
+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
|
||||
+33
@@ -61,6 +61,7 @@ import org.apache.fineract.client.models.AdvancedPaymentData;
|
||||
import org.apache.fineract.client.models.AllowAttributeOverrides;
|
||||
import org.apache.fineract.client.models.BusinessDateRequest;
|
||||
import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdChargesChargeIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdStatus;
|
||||
@@ -71,6 +72,7 @@ import org.apache.fineract.client.models.LoanPointInTimeData;
|
||||
import org.apache.fineract.client.models.PaymentAllocationOrder;
|
||||
import org.apache.fineract.client.models.PostChargesResponse;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
||||
@@ -796,6 +798,14 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
return chargeId.longValue();
|
||||
}
|
||||
|
||||
protected Long createOverduePenaltyPercentageCharge(double percentageAmount, Integer feeFrequency, int feeInterval) {
|
||||
Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||
ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentageWithFeeInterval(String.valueOf(percentageAmount),
|
||||
feeFrequency, feeInterval));
|
||||
assertNotNull(chargeId);
|
||||
return chargeId.longValue();
|
||||
}
|
||||
|
||||
protected void verifyRepaymentSchedule(GetLoansLoanIdResponse savedLoanResponse, GetLoansLoanIdResponse actualLoanResponse,
|
||||
int totalPeriods, int identicalPeriods) {
|
||||
List<GetLoansLoanIdRepaymentPeriod> savedPeriods = savedLoanResponse.getRepaymentSchedule().getPeriods();
|
||||
@@ -934,6 +944,18 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
protected void runFromToInclusive(String fromDate, String toDate, Runnable runnable) {
|
||||
DateTimeFormatter format = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
|
||||
LocalDate startDate = LocalDate.parse(fromDate, format);
|
||||
LocalDate endDate = LocalDate.parse(toDate, format);
|
||||
|
||||
LocalDate currentDate = startDate;
|
||||
while (currentDate.isBefore(endDate) || currentDate.isEqual(endDate)) {
|
||||
runAt(format.format(currentDate), runnable);
|
||||
currentDate = currentDate.plusDays(1);
|
||||
}
|
||||
}
|
||||
|
||||
protected void runAt(String date, Runnable runnable) {
|
||||
try {
|
||||
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
|
||||
@@ -1092,6 +1114,17 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
return loanTransactionHelper.addChargeForLoan(loanId.intValue(), payload, responseSpec);
|
||||
}
|
||||
|
||||
protected List<GetLoansLoanIdChargesChargeIdResponse> getOverdueInstallmentLoanCharges(Long loanId) {
|
||||
return ok(fineractClient().loanCharges.retrieveAllLoanCharges(loanId)).stream() //
|
||||
.filter(ch -> ch.getChargeTimeType().getId().intValue() == ChargesHelper.CHARGE_OVERDUE_INSTALLMENT_FEE) //
|
||||
.toList(); //
|
||||
}
|
||||
|
||||
protected void deactivateOverdueLoanCharges(Long loanId, String fromDueDate) {
|
||||
ok(fineractClient().loanCharges.executeLoanCharge(loanId,
|
||||
new PostLoansLoanIdChargesRequest().dueDate(fromDueDate).dateFormat(DATETIME_PATTERN).locale("en"), "deactivateOverdue"));
|
||||
}
|
||||
|
||||
protected void waiveLoanCharge(Long loanId, Long chargeId, Integer installmentNumber) {
|
||||
String payload = LoanTransactionHelper.getWaiveChargeJSON(installmentNumber.toString());
|
||||
loanTransactionHelper.waiveChargesForLoan(loanId.intValue(), chargeId.intValue(), payload);
|
||||
|
||||
+36
-17
@@ -50,19 +50,19 @@ public final class ChargesHelper {
|
||||
private static final Integer CHARGE_APPLIES_TO_CLIENT = 3;
|
||||
private static final Integer CHARGE_APPLIES_TO_SHARES = 4;
|
||||
|
||||
private static final Integer CHARGE_DISBURSEMENT_FEE = 1;
|
||||
private static final Integer CHARGE_SPECIFIED_DUE_DATE = 2;
|
||||
private static final Integer CHARGE_SAVINGS_ACTIVATION_FEE = 3;
|
||||
private static final Integer CHARGE_WITHDRAWAL_FEE = 5;
|
||||
private static final Integer CHARGE_ANNUAL_FEE = 6;
|
||||
private static final Integer CHARGE_MONTHLY_FEE = 7;
|
||||
private static final Integer CHARGE_INSTALLMENT_FEE = 8;
|
||||
private static final Integer CHARGE_OVERDUE_INSTALLMENT_FEE = 9;
|
||||
private static final Integer CHARGE_OVERDRAFT_FEE = 10;
|
||||
private static final Integer WEEKLY_FEE = 11;
|
||||
private static final Integer SHAREACCOUNT_ACTIVATION = 13;
|
||||
private static final Integer SHARE_PURCHASE = 14;
|
||||
private static final Integer SHARE_REDEEM = 15;
|
||||
public static final Integer CHARGE_DISBURSEMENT_FEE = 1;
|
||||
public static final Integer CHARGE_SPECIFIED_DUE_DATE = 2;
|
||||
public static final Integer CHARGE_SAVINGS_ACTIVATION_FEE = 3;
|
||||
public static final Integer CHARGE_WITHDRAWAL_FEE = 5;
|
||||
public static final Integer CHARGE_ANNUAL_FEE = 6;
|
||||
public static final Integer CHARGE_MONTHLY_FEE = 7;
|
||||
public static final Integer CHARGE_INSTALLMENT_FEE = 8;
|
||||
public static final Integer CHARGE_OVERDUE_INSTALLMENT_FEE = 9;
|
||||
public static final Integer CHARGE_OVERDRAFT_FEE = 10;
|
||||
public static final Integer WEEKLY_FEE = 11;
|
||||
public static final Integer SHAREACCOUNT_ACTIVATION = 13;
|
||||
public static final Integer SHARE_PURCHASE = 14;
|
||||
public static final Integer SHARE_REDEEM = 15;
|
||||
|
||||
private static final Integer CHARGE_SAVINGS_NO_ACTIVITY_FEE = 16;
|
||||
|
||||
@@ -76,10 +76,10 @@ public final class ChargesHelper {
|
||||
private static final Integer CHARGE_PAYMENT_MODE_REGULAR = 0;
|
||||
private static final Integer CHARGE_PAYMENT_MODE_ACCOUNT_TRANSFER = 1;
|
||||
|
||||
private static final Integer CHARGE_FEE_FREQUENCY_DAYS = 0;
|
||||
private static final Integer CHARGE_FEE_FREQUENCY_WEEKS = 1;
|
||||
private static final Integer CHARGE_FEE_FREQUENCY_MONTHS = 2;
|
||||
private static final Integer CHARGE_FEE_FREQUENCY_YEARS = 3;
|
||||
public static final Integer CHARGE_FEE_FREQUENCY_DAYS = 0;
|
||||
public static final Integer CHARGE_FEE_FREQUENCY_WEEKS = 1;
|
||||
public static final Integer CHARGE_FEE_FREQUENCY_MONTHS = 2;
|
||||
public static final Integer CHARGE_FEE_FREQUENCY_YEARS = 3;
|
||||
|
||||
private static final boolean ACTIVE = true;
|
||||
private static final boolean PENALTY = true;
|
||||
@@ -491,6 +491,25 @@ public final class ChargesHelper {
|
||||
return chargesCreateJson;
|
||||
}
|
||||
|
||||
// TODO: Rewrite to use fineract-client instead!
|
||||
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
|
||||
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
|
||||
@Deprecated(forRemoval = true)
|
||||
public static String getLoanOverdueFeeJSONWithCalculationTypePercentageWithFeeInterval(String penaltyPercentageAmount,
|
||||
Integer feeFrequency, int feeInterval) {
|
||||
final HashMap<String, Object> map = populateDefaultsForLoan();
|
||||
map.put("penalty", ChargesHelper.PENALTY);
|
||||
map.put("amount", penaltyPercentageAmount);
|
||||
map.put("chargePaymentMode", ChargesHelper.CHARGE_PAYMENT_MODE_REGULAR);
|
||||
map.put("chargeTimeType", CHARGE_OVERDUE_INSTALLMENT_FEE);
|
||||
map.put("chargeCalculationType", ChargesHelper.CHARGE_CALCULATION_TYPE_PERCENTAGE_AMOUNT_AND_INTEREST);
|
||||
map.put("feeFrequency", feeFrequency);
|
||||
map.put("feeInterval", feeInterval);
|
||||
String chargesCreateJson = new Gson().toJson(map);
|
||||
LOG.info("{}", chargesCreateJson);
|
||||
return chargesCreateJson;
|
||||
}
|
||||
|
||||
// TODO: Rewrite to use fineract-client instead!
|
||||
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
|
||||
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
|
||||
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* 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.integrationtests.loan.penalty;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.apache.fineract.client.models.ChargeData;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.PostLoansRequest;
|
||||
import org.apache.fineract.client.models.PostLoansResponse;
|
||||
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
|
||||
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
|
||||
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
|
||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||
import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class LoanPenaltyBackdatedTransactionTest extends BaseLoanIntegrationTest {
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
PutGlobalConfigurationsRequest request = new PutGlobalConfigurationsRequest().value(0L).enabled(true);
|
||||
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.PENALTY_WAIT_PERIOD, request);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void after() {
|
||||
// go back to defaults
|
||||
PutGlobalConfigurationsRequest request = new PutGlobalConfigurationsRequest().value(2L).enabled(true);
|
||||
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.PENALTY_WAIT_PERIOD, request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_PenaltyRecalculationWorksForBackdatedTx_WhenCumulative_1() {
|
||||
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||
|
||||
runAt("01 January 2023", () -> {
|
||||
// Create Client
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
|
||||
int numberOfRepayments = 3;
|
||||
int repaymentEvery = 4;
|
||||
|
||||
// Create charges
|
||||
double chargeAmount = 1.0;
|
||||
Long chargeId = createOverduePenaltyPercentageCharge(chargeAmount, ChargesHelper.CHARGE_FEE_FREQUENCY_DAYS, 1);
|
||||
|
||||
// Create Loan Product
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
|
||||
.graceOnArrearsAgeing(0).numberOfRepayments(numberOfRepayments) //
|
||||
.repaymentEvery(repaymentEvery) //
|
||||
.installmentAmountInMultiplesOf(null) //
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.DAYS.longValue()) //
|
||||
.interestType(InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
|
||||
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
|
||||
.isInterestRecalculationEnabled(true)//
|
||||
.recalculationRestFrequencyInterval(1)//
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
|
||||
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
|
||||
.allowPartialPeriodInterestCalcualtion(false)//
|
||||
.disallowExpectedDisbursements(false)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||
.overAppliedNumber(null)//
|
||||
.overAppliedCalculationType(null)//
|
||||
.multiDisburseLoan(null)//
|
||||
.charges(List.of(new ChargeData().id(chargeId)));//
|
||||
|
||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||
Long loanProductId = loanProductResponse.getResourceId();
|
||||
|
||||
// Apply and Approve Loan
|
||||
double amount = 5000.0;
|
||||
|
||||
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
|
||||
.repaymentEvery(repaymentEvery)//
|
||||
.loanTermFrequency(numberOfRepayments * repaymentEvery)//
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.DAYS)//
|
||||
.loanTermFrequencyType(RepaymentFrequencyType.DAYS)//
|
||||
.interestType(InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);
|
||||
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||
|
||||
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||
approveLoanRequest(amount, "01 January 2023"));
|
||||
|
||||
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||
Long loanId = aLoanId.get();
|
||||
|
||||
// disburse Loan
|
||||
disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 2023");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023") //
|
||||
);
|
||||
});
|
||||
|
||||
runFromToInclusive("01 January 2023", "09 January 2023", () -> {
|
||||
// run accrual posting
|
||||
schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue loans");
|
||||
schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
|
||||
});
|
||||
|
||||
runAt("09 January 2023", () -> {
|
||||
Long loanId = aLoanId.get();
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023", 5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 50.01, 0.0, 0.0) //
|
||||
);
|
||||
|
||||
// repay 1k
|
||||
addRepaymentForLoan(loanId, 1000.0, "07 January 2023");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023", 5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(1000.0, "Repayment", "07 January 2023", 4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 50.01, 0.0, 0.0) //
|
||||
);
|
||||
|
||||
// reverse accruals
|
||||
deactivateOverdueLoanCharges(loanId, "07 January 2023");
|
||||
|
||||
// run accrual posting
|
||||
schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue loans");
|
||||
schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023", 5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(1000.0, "Repayment", "07 January 2023", 4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 50.01, 0.0, 0.0, true), //
|
||||
transaction(30.33, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 30.33, 0.0, 0.0) //
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_PenaltyRecalculationWorksForBackdatedTx_WhenCumulative_2() {
|
||||
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||
|
||||
runAt("01 January 2023", () -> {
|
||||
// Create Client
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
|
||||
int numberOfRepayments = 3;
|
||||
int repaymentEvery = 4;
|
||||
|
||||
// Create charges
|
||||
double chargeAmount = 1.0;
|
||||
Long chargeId = createOverduePenaltyPercentageCharge(chargeAmount, ChargesHelper.CHARGE_FEE_FREQUENCY_DAYS, 1);
|
||||
|
||||
// Create Loan Product
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
|
||||
.graceOnArrearsAgeing(0).numberOfRepayments(numberOfRepayments) //
|
||||
.repaymentEvery(repaymentEvery) //
|
||||
.installmentAmountInMultiplesOf(null) //
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.DAYS.longValue()) //
|
||||
.interestType(InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
|
||||
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
|
||||
.isInterestRecalculationEnabled(true)//
|
||||
.recalculationRestFrequencyInterval(1)//
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
|
||||
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
|
||||
.allowPartialPeriodInterestCalcualtion(false)//
|
||||
.disallowExpectedDisbursements(false)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||
.overAppliedNumber(null)//
|
||||
.overAppliedCalculationType(null)//
|
||||
.multiDisburseLoan(null)//
|
||||
.charges(List.of(new ChargeData().id(chargeId)));//
|
||||
|
||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||
Long loanProductId = loanProductResponse.getResourceId();
|
||||
|
||||
// Apply and Approve Loan
|
||||
double amount = 5000.0;
|
||||
|
||||
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
|
||||
.repaymentEvery(repaymentEvery)//
|
||||
.loanTermFrequency(numberOfRepayments * repaymentEvery)//
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.DAYS)//
|
||||
.loanTermFrequencyType(RepaymentFrequencyType.DAYS)//
|
||||
.interestType(InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);
|
||||
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||
|
||||
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||
approveLoanRequest(amount, "01 January 2023"));
|
||||
|
||||
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||
Long loanId = aLoanId.get();
|
||||
|
||||
// disburse Loan
|
||||
disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 2023");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023") //
|
||||
);
|
||||
});
|
||||
|
||||
runFromToInclusive("01 January 2023", "09 January 2023", () -> {
|
||||
// run accrual posting
|
||||
schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue loans");
|
||||
schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
|
||||
});
|
||||
|
||||
runAt("09 January 2023", () -> {
|
||||
Long loanId = aLoanId.get();
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023", 5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 50.01, 0.0, 0.0) //
|
||||
);
|
||||
|
||||
// repay 1k
|
||||
addRepaymentForLoan(loanId, 1000.0, "07 January 2023");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023", 5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(1000.0, "Repayment", "07 January 2023", 4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 50.01, 0.0, 0.0) //
|
||||
);
|
||||
|
||||
// reverse accruals
|
||||
deactivateOverdueLoanCharges(loanId, "05 January 2023");
|
||||
|
||||
// run accrual posting
|
||||
schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue loans");
|
||||
schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023", 5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 0.0, 0.0, 16.67, 0.0, 0.0, true), //
|
||||
transaction(6.83, "Accrual", "05 January 2023", 0.0, 0.0, 0.0, 0.0, 6.83, 0.0, 0.0), //
|
||||
transaction(1000.0, "Repayment", "07 January 2023", 4006.83, 993.17, 0.0, 0.0, 6.83, 0.0, 0.0), //
|
||||
transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 50.01, 0.0, 0.0, true), //
|
||||
transaction(20.49, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 20.49, 0.0, 0.0) //
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_PenaltyRecalculationWorksForBackdatedTx_WhenCumulative_3() {
|
||||
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||
|
||||
runAt("01 January 2023", () -> {
|
||||
// Create Client
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
|
||||
int numberOfRepayments = 3;
|
||||
int repaymentEvery = 4;
|
||||
|
||||
// Create charges
|
||||
double chargeAmount = 1.0;
|
||||
Long chargeId = createOverduePenaltyPercentageCharge(chargeAmount, ChargesHelper.CHARGE_FEE_FREQUENCY_DAYS, 1);
|
||||
|
||||
// Create Loan Product
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
|
||||
.graceOnArrearsAgeing(0).numberOfRepayments(numberOfRepayments) //
|
||||
.repaymentEvery(repaymentEvery) //
|
||||
.installmentAmountInMultiplesOf(null) //
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.DAYS.longValue()) //
|
||||
.interestType(InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
|
||||
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
|
||||
.isInterestRecalculationEnabled(true)//
|
||||
.recalculationRestFrequencyInterval(1)//
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
|
||||
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
|
||||
.allowPartialPeriodInterestCalcualtion(false)//
|
||||
.disallowExpectedDisbursements(false)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||
.overAppliedNumber(null)//
|
||||
.overAppliedCalculationType(null)//
|
||||
.multiDisburseLoan(null)//
|
||||
.charges(List.of(new ChargeData().id(chargeId)));//
|
||||
|
||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||
Long loanProductId = loanProductResponse.getResourceId();
|
||||
|
||||
// Apply and Approve Loan
|
||||
double amount = 5000.0;
|
||||
|
||||
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
|
||||
.repaymentEvery(repaymentEvery)//
|
||||
.loanTermFrequency(numberOfRepayments * repaymentEvery)//
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.DAYS)//
|
||||
.loanTermFrequencyType(RepaymentFrequencyType.DAYS)//
|
||||
.interestType(InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);
|
||||
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||
|
||||
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||
approveLoanRequest(amount, "01 January 2023"));
|
||||
|
||||
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||
Long loanId = aLoanId.get();
|
||||
|
||||
// disburse Loan
|
||||
disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 2023");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023") //
|
||||
);
|
||||
});
|
||||
|
||||
runFromToInclusive("01 January 2023", "09 January 2023", () -> {
|
||||
// run accrual posting
|
||||
schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue loans");
|
||||
schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
|
||||
});
|
||||
|
||||
runAt("09 January 2023", () -> {
|
||||
Long loanId = aLoanId.get();
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023", 5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 50.01, 0.0, 0.0) //
|
||||
);
|
||||
|
||||
// repay 1k
|
||||
addRepaymentForLoan(loanId, 1000.0, "07 January 2023");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023", 5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(1000.0, "Repayment", "07 January 2023", 4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 50.01, 0.0, 0.0) //
|
||||
);
|
||||
|
||||
// reverse accruals
|
||||
deactivateOverdueLoanCharges(loanId, "07 January 2023");
|
||||
|
||||
// run accrual posting
|
||||
schedulerJobHelper.executeAndAwaitJob("Apply penalty to overdue loans");
|
||||
schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023", 5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(1000.0, "Repayment", "07 January 2023", 4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 50.01, 0.0, 0.0, true), //
|
||||
transaction(30.33, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 30.33, 0.0, 0.0) //
|
||||
);
|
||||
});
|
||||
|
||||
runAt("10 January 2023", () -> {
|
||||
Long loanId = aLoanId.get();
|
||||
|
||||
// repay 1k
|
||||
addRepaymentForLoan(loanId, 1000.0, "10 January 2023");
|
||||
|
||||
// verify transactions
|
||||
verifyTransactions(loanId, //
|
||||
transaction(5000.0, "Disbursement", "01 January 2023", 5000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(16.67, "Accrual", "05 January 2023", 0.0, 0.0, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(1000.0, "Repayment", "07 January 2023", 4016.67, 983.33, 0.0, 0.0, 16.67, 0.0, 0.0), //
|
||||
transaction(50.01, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 50.01, 0.0, 0.0, true), //
|
||||
transaction(30.33, "Accrual", "09 January 2023", 0.0, 0.0, 0.0, 0.0, 30.33, 0.0, 0.0), //
|
||||
transaction(1000.0, "Repayment", "10 January 2023", 3047.0, 969.67, 0.0, 0.0, 30.33, 0.0, 0.0) //
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user