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;
|
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) {
|
public CommandWrapperBuilder deleteLoanCharge(final Long loanId, final Long loanChargeId) {
|
||||||
this.actionName = "DELETE";
|
this.actionName = "DELETE";
|
||||||
this.entityName = "LOANCHARGE";
|
this.entityName = "LOANCHARGE";
|
||||||
|
|||||||
+8
@@ -302,6 +302,10 @@ public final class DateUtils {
|
|||||||
return isAfterBusinessDate(localDate);
|
return isAfterBusinessDate(localDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isDateInThePast(final LocalDate localDate) {
|
||||||
|
return isBeforeBusinessDate(localDate);
|
||||||
|
}
|
||||||
|
|
||||||
public static int compare(LocalDate first, LocalDate second) {
|
public static int compare(LocalDate first, LocalDate second) {
|
||||||
return compare(first, second, true);
|
return compare(first, second, true);
|
||||||
}
|
}
|
||||||
@@ -334,6 +338,10 @@ public final class DateUtils {
|
|||||||
return first != null && (second == null || first.isAfter(second));
|
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) {
|
public static long getDifference(LocalDate first, LocalDate second, @NotNull ChronoUnit unit) {
|
||||||
if (first == null || second == null) {
|
if (first == null || second == null) {
|
||||||
throw new IllegalArgumentException("Dates must not be null to get difference");
|
throw new IllegalArgumentException("Dates must not be null to get difference");
|
||||||
|
|||||||
+9
@@ -18,6 +18,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.loanaccount.domain;
|
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.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
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)
|
@Query(FIND_ID_BY_EXTERNAL_ID)
|
||||||
Long findIdByExternalId(@Param("externalId") ExternalId externalId);
|
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 adjustmentForLoanCharge(Long loanId, Long loanChargeId, JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult deactivateOverdueLoanCharge(Long loanId, JsonCommand command);
|
||||||
|
|
||||||
void applyOverdueChargesForLoan(Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList);
|
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_PAY = "pay";
|
||||||
public static final String COMMAND_WAIVE = "waive";
|
public static final String COMMAND_WAIVE = "waive";
|
||||||
public static final String COMMAND_ADJUSTMENT = "adjustment";
|
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<>(
|
private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(
|
||||||
Arrays.asList("id", "chargeId", "name", "penalty", "chargeTimeType", "dueAsOfDate", "chargeCalculationType", "percentage",
|
Arrays.asList("id", "chargeId", "name", "penalty", "chargeTimeType", "dueAsOfDate", "chargeCalculationType", "percentage",
|
||||||
"amountPercentageAppliedTo", "currency", "amountWaived", "amountWrittenOff", "amountOutstanding", "amountOrPercentage",
|
"amountPercentageAppliedTo", "currency", "amountWaived", "amountWrittenOff", "amountOutstanding", "amountOrPercentage",
|
||||||
@@ -463,6 +464,10 @@ public class LoanChargesApiResource {
|
|||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().payLoanCharge(resolvedLoanId, null)
|
final CommandWrapper commandRequest = new CommandWrapperBuilder().payLoanCharge(resolvedLoanId, null)
|
||||||
.withJson(apiRequestBodyAsJson).build();
|
.withJson(apiRequestBodyAsJson).build();
|
||||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
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 {
|
} else {
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder().createLoanCharge(resolvedLoanId)
|
final CommandWrapper commandRequest = new CommandWrapperBuilder().createLoanCharge(resolvedLoanId)
|
||||||
.withJson(apiRequestBodyAsJson).build();
|
.withJson(apiRequestBodyAsJson).build();
|
||||||
|
|||||||
+4
-1
@@ -294,6 +294,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
|||||||
|| !loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
|
|| !loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LoanInterestRecalculationDetails recalculationDetails = loan.getLoanInterestRecalculationDetails();
|
LoanInterestRecalculationDetails recalculationDetails = loan.getLoanInterestRecalculationDetails();
|
||||||
if (recalculationDetails != null && recalculationDetails.isCompoundingToBePostedAsTransaction()) {
|
if (recalculationDetails != null && recalculationDetails.isCompoundingToBePostedAsTransaction()) {
|
||||||
return;
|
return;
|
||||||
@@ -523,7 +524,9 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
|
|||||||
loanCharges = loan.getLoanCharges(lc -> !lc.isDueAtDisbursement() && (lc.isInstalmentFee() ? !DateUtils.isBefore(tillDate, dueDate)
|
loanCharges = loan.getLoanCharges(lc -> !lc.isDueAtDisbursement() && (lc.isInstalmentFee() ? !DateUtils.isBefore(tillDate, dueDate)
|
||||||
: isChargeDue(lc, tillDate, chargeOnDueDate, installment, period.isFirstPeriod())));
|
: isChargeDue(lc, tillDate, chargeOnDueDate, installment, period.isFirstPeriod())));
|
||||||
for (LoanCharge loanCharge : loanCharges) {
|
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.InstallmentNotFoundException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeAdjustmentException;
|
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.LoanChargeRefundException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
|
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.LoanChargeApiJsonValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
|
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.data.LoanOverdueDTO;
|
||||||
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
|
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
|
||||||
import org.apache.fineract.portfolio.note.domain.Note;
|
import org.apache.fineract.portfolio.note.domain.Note;
|
||||||
@@ -174,6 +177,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
|||||||
private final LoanScheduleService loanScheduleService;
|
private final LoanScheduleService loanScheduleService;
|
||||||
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||||
private final LoanAccountService loanAccountService;
|
private final LoanAccountService loanAccountService;
|
||||||
|
private final LoanAdjustmentService loanAdjustmentService;
|
||||||
|
|
||||||
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
|
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
|
||||||
return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate());
|
return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate());
|
||||||
@@ -776,6 +780,49 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
|||||||
.build();
|
.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
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public void applyOverdueChargesForLoan(final Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList) {
|
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 JsonElement parsedCommand = this.fromApiJsonHelper.parse(overdueInstallment.toString());
|
||||||
final JsonCommand command = JsonCommand.from(overdueInstallment.toString(), parsedCommand, this.fromApiJsonHelper, null, null,
|
final JsonCommand command = JsonCommand.from(overdueInstallment.toString(), parsedCommand, this.fromApiJsonHelper, null, null,
|
||||||
null, null, null, loanId, null, null, null, null, null, null, null, null);
|
null, null, null, loanId, null, null, null, null, null, null, null, null);
|
||||||
|
|
||||||
LoanOverdueDTO overdueDTO = applyChargeToOverdueLoanInstallment(loan, overdueInstallment.getChargeId(),
|
LoanOverdueDTO overdueDTO = applyChargeToOverdueLoanInstallment(loan, overdueInstallment.getChargeId(),
|
||||||
overdueInstallment.getPeriodNumber(), command);
|
overdueInstallment.getPeriodNumber(), command);
|
||||||
|
|
||||||
loan = overdueDTO.getLoan();
|
loan = overdueDTO.getLoan();
|
||||||
runInterestRecalculation = runInterestRecalculation || overdueDTO.isRunInterestRecalculation();
|
runInterestRecalculation = runInterestRecalculation || overdueDTO.isRunInterestRecalculation();
|
||||||
if (DateUtils.isAfter(recalculateFrom, overdueDTO.getRecalculateFrom())) {
|
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.guarantor.service.GuarantorDomainService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
|
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.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.loanschedule.service.LoanScheduleHistoryWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
|
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
|
||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
|
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.LoanOfficerValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
|
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.domain.LoanProduct;
|
||||||
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
|
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
|
||||||
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
|
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
|
||||||
@@ -278,6 +279,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
private final LoanOfficerService loanOfficerService;
|
private final LoanOfficerService loanOfficerService;
|
||||||
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||||
private final LoanAccountService loanAccountService;
|
private final LoanAccountService loanAccountService;
|
||||||
|
private final LoanJournalEntryPoster journalEntryPoster;
|
||||||
|
private final LoanAdjustmentService loanAdjustmentService;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -534,7 +537,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction));
|
||||||
}
|
}
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
@@ -816,7 +819,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
if (!changes.isEmpty()) {
|
if (!changes.isEmpty()) {
|
||||||
createNote(loan, command, changes);
|
createNote(loan, command, changes);
|
||||||
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
}
|
}
|
||||||
final Set<LoanCharge> loanCharges = loan.getActiveCharges();
|
final Set<LoanCharge> loanCharges = loan.getActiveCharges();
|
||||||
@@ -1165,7 +1168,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
|
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);
|
"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 LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||||
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
|
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
|
||||||
@@ -1354,181 +1359,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
changes.put("dateFormat", command.dateFormat());
|
changes.put("dateFormat", command.dateFormat());
|
||||||
changes.put("paymentTypeId", command.longValueOfParameterNamed("paymentTypeId"));
|
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);
|
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()) {
|
return loanAdjustmentService.adjustLoanTransaction(loan, transactionToAdjust, parameter, commandId, changes);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -1611,7 +1448,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
this.noteRepository.save(note);
|
this.noteRepository.save(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
this.loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate);
|
this.loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate);
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargebackTransactionBusinessEvent(newTransaction));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargebackTransactionBusinessEvent(newTransaction));
|
||||||
@@ -1714,7 +1551,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction));
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
.withCommandId(command.commandId()) //
|
.withCommandId(command.commandId()) //
|
||||||
@@ -1808,7 +1645,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(loanTransaction));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(loanTransaction));
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
|
|
||||||
builder.withEntityId(loanTransaction.getId()).withEntityExternalId(loanTransaction.getExternalId());
|
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
|
// disable all active standing instructions linked to the loan
|
||||||
this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan);
|
this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan);
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
|
|
||||||
CommandProcessingResult result;
|
CommandProcessingResult result;
|
||||||
@@ -2021,7 +1858,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
this.loanTransactionRepository.saveAndFlush(newTransferTransaction);
|
this.loanTransactionRepository.saveAndFlush(newTransferTransaction);
|
||||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanInitiateTransferBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanInitiateTransferBusinessEvent(loan));
|
||||||
return newTransferTransaction;
|
return newTransferTransaction;
|
||||||
}
|
}
|
||||||
@@ -2051,7 +1888,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
|
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
|
||||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAcceptTransferBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanAcceptTransferBusinessEvent(loan));
|
||||||
|
|
||||||
return newTransferAcceptanceTransaction;
|
return newTransferAcceptanceTransaction;
|
||||||
@@ -2077,7 +1914,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
|
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
|
||||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanWithdrawTransferBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanWithdrawTransferBusinessEvent(loan));
|
||||||
|
|
||||||
return newTransferAcceptanceTransaction;
|
return newTransferAcceptanceTransaction;
|
||||||
@@ -2209,25 +2046,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
.build();
|
.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
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public void applyMeetingDateChanges(final Calendar calendar, final Collection<CalendarInstance> loanCalendarInstances) {
|
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());
|
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
@@ -2647,7 +2465,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
false);
|
false);
|
||||||
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
|
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
@@ -2709,7 +2527,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
false);
|
false);
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan));
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
return loan;
|
return loan;
|
||||||
}
|
}
|
||||||
@@ -3129,7 +2947,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeOffPostBusinessEvent(chargeOffTransaction));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeOffPostBusinessEvent(chargeOffTransaction));
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
.withCommandId(command.commandId()) //
|
.withCommandId(command.commandId()) //
|
||||||
@@ -3192,7 +3010,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
reprocessLoanTransactionsService.reprocessTransactions(loan);
|
reprocessLoanTransactionsService.reprocessTransactions(loan);
|
||||||
|
|
||||||
saveLoanWithDataIntegrityViolationChecks(loan);
|
saveLoanWithDataIntegrityViolationChecks(loan);
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoChargeOffBusinessEvent(chargedOffTransaction));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoChargeOffBusinessEvent(chargedOffTransaction));
|
||||||
|
|
||||||
return new CommandProcessingResultBuilder() //
|
return new CommandProcessingResultBuilder() //
|
||||||
@@ -3267,7 +3085,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||||
|
|
||||||
// Create journal entries for the new transaction(s)
|
// Create journal entries for the new transaction(s)
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||||
|
|
||||||
Long entityId = refundTransaction.getId();
|
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.LoanDisbursementService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerServiceImpl;
|
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.LoanOfficerService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl;
|
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.ReplayedTransactionBusinessEventService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
|
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.domain.LoanProductRepository;
|
||||||
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
|
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
|
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
|
||||||
@@ -296,7 +298,7 @@ public class LoanAccountConfiguration {
|
|||||||
LoanAccrualsProcessingService loanAccrualsProcessingService,
|
LoanAccrualsProcessingService loanAccrualsProcessingService,
|
||||||
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanChargeValidator loanChargeValidator,
|
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanChargeValidator loanChargeValidator,
|
||||||
LoanScheduleService loanScheduleService, ReprocessLoanTransactionsService reprocessLoanTransactionsService,
|
LoanScheduleService loanScheduleService, ReprocessLoanTransactionsService reprocessLoanTransactionsService,
|
||||||
LoanAccountService loanAccountService) {
|
LoanAccountService loanAccountService, LoanAdjustmentService loanAdjustmentService) {
|
||||||
return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository,
|
return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository,
|
||||||
businessEventNotifierService, loanTransactionRepository, accountTransfersWritePlatformService, loanRepositoryWrapper,
|
businessEventNotifierService, loanTransactionRepository, accountTransfersWritePlatformService, loanRepositoryWrapper,
|
||||||
journalEntryWritePlatformService, loanAccountDomainService, loanChargeRepository, loanWritePlatformService, loanUtilService,
|
journalEntryWritePlatformService, loanAccountDomainService, loanChargeRepository, loanWritePlatformService, loanUtilService,
|
||||||
@@ -304,7 +306,7 @@ public class LoanAccountConfiguration {
|
|||||||
configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory,
|
configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory,
|
||||||
accountTransferDetailRepository, loanChargeAssembler, paymentDetailWritePlatformService, noteRepository,
|
accountTransferDetailRepository, loanChargeAssembler, paymentDetailWritePlatformService, noteRepository,
|
||||||
loanAccrualTransactionBusinessEventService, loanAccrualsProcessingService, loanDownPaymentTransactionValidator,
|
loanAccrualTransactionBusinessEventService, loanAccrualsProcessingService, loanDownPaymentTransactionValidator,
|
||||||
loanChargeValidator, loanScheduleService, reprocessLoanTransactionsService, loanAccountService);
|
loanChargeValidator, loanScheduleService, reprocessLoanTransactionsService, loanAccountService, loanAdjustmentService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -392,7 +394,8 @@ public class LoanAccountConfiguration {
|
|||||||
LoanOfficerValidator loanOfficerValidator, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator,
|
LoanOfficerValidator loanOfficerValidator, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator,
|
||||||
LoanDisbursementService loanDisbursementService, LoanScheduleService loanScheduleService,
|
LoanDisbursementService loanDisbursementService, LoanScheduleService loanScheduleService,
|
||||||
LoanChargeValidator loanChargeValidator, LoanOfficerService loanOfficerService,
|
LoanChargeValidator loanChargeValidator, LoanOfficerService loanOfficerService,
|
||||||
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanAccountService loanAccountService) {
|
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanAccountService loanAccountService,
|
||||||
|
LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService) {
|
||||||
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
|
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
|
||||||
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
|
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
|
||||||
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
|
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
|
||||||
@@ -406,7 +409,7 @@ public class LoanAccountConfiguration {
|
|||||||
loanAccountLockService, externalIdFactory, loanAccrualTransactionBusinessEventService, errorHandler,
|
loanAccountLockService, externalIdFactory, loanAccrualTransactionBusinessEventService, errorHandler,
|
||||||
loanDownPaymentHandlerService, loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator,
|
loanDownPaymentHandlerService, loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator,
|
||||||
loanDownPaymentTransactionValidator, loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService,
|
loanDownPaymentTransactionValidator, loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService,
|
||||||
reprocessLoanTransactionsService, loanAccountService);
|
reprocessLoanTransactionsService, loanAccountService, journalEntryPoster, loanAdjustmentService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@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.LoanChargeValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
|
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.note.domain.NoteRepository;
|
||||||
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository;
|
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository;
|
||||||
@@ -90,10 +91,10 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class LoanWritePlatformServiceJpaRepositoryImplTest {
|
class LoanAdjustmentServiceImplTest {
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private LoanWritePlatformServiceJpaRepositoryImpl loanWritePlatformService;
|
private LoanAdjustmentServiceImpl underTest;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
|
private LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
|
||||||
@@ -240,7 +241,7 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
|
|||||||
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
|
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
loanWritePlatformService.adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
|
underTest.adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
|
||||||
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalExternalId);
|
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalExternalId);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -285,7 +286,7 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
|
|||||||
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
|
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
loanWritePlatformService.adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
|
underTest.adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
|
||||||
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalExternalId);
|
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalExternalId);
|
||||||
|
|
||||||
// Assert
|
// 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.AllowAttributeOverrides;
|
||||||
import org.apache.fineract.client.models.BusinessDateRequest;
|
import org.apache.fineract.client.models.BusinessDateRequest;
|
||||||
import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
|
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.GetLoansLoanIdRepaymentPeriod;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdStatus;
|
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.PaymentAllocationOrder;
|
||||||
import org.apache.fineract.client.models.PostChargesResponse;
|
import org.apache.fineract.client.models.PostChargesResponse;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
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.PostLoansLoanIdChargesResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
|
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
||||||
@@ -796,6 +798,14 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
|||||||
return chargeId.longValue();
|
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,
|
protected void verifyRepaymentSchedule(GetLoansLoanIdResponse savedLoanResponse, GetLoansLoanIdResponse actualLoanResponse,
|
||||||
int totalPeriods, int identicalPeriods) {
|
int totalPeriods, int identicalPeriods) {
|
||||||
List<GetLoansLoanIdRepaymentPeriod> savedPeriods = savedLoanResponse.getRepaymentSchedule().getPeriods();
|
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) {
|
protected void runAt(String date, Runnable runnable) {
|
||||||
try {
|
try {
|
||||||
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
|
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
|
||||||
@@ -1092,6 +1114,17 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
|||||||
return loanTransactionHelper.addChargeForLoan(loanId.intValue(), payload, responseSpec);
|
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) {
|
protected void waiveLoanCharge(Long loanId, Long chargeId, Integer installmentNumber) {
|
||||||
String payload = LoanTransactionHelper.getWaiveChargeJSON(installmentNumber.toString());
|
String payload = LoanTransactionHelper.getWaiveChargeJSON(installmentNumber.toString());
|
||||||
loanTransactionHelper.waiveChargesForLoan(loanId.intValue(), chargeId.intValue(), payload);
|
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_CLIENT = 3;
|
||||||
private static final Integer CHARGE_APPLIES_TO_SHARES = 4;
|
private static final Integer CHARGE_APPLIES_TO_SHARES = 4;
|
||||||
|
|
||||||
private static final Integer CHARGE_DISBURSEMENT_FEE = 1;
|
public static final Integer CHARGE_DISBURSEMENT_FEE = 1;
|
||||||
private static final Integer CHARGE_SPECIFIED_DUE_DATE = 2;
|
public static final Integer CHARGE_SPECIFIED_DUE_DATE = 2;
|
||||||
private static final Integer CHARGE_SAVINGS_ACTIVATION_FEE = 3;
|
public static final Integer CHARGE_SAVINGS_ACTIVATION_FEE = 3;
|
||||||
private static final Integer CHARGE_WITHDRAWAL_FEE = 5;
|
public static final Integer CHARGE_WITHDRAWAL_FEE = 5;
|
||||||
private static final Integer CHARGE_ANNUAL_FEE = 6;
|
public static final Integer CHARGE_ANNUAL_FEE = 6;
|
||||||
private static final Integer CHARGE_MONTHLY_FEE = 7;
|
public static final Integer CHARGE_MONTHLY_FEE = 7;
|
||||||
private static final Integer CHARGE_INSTALLMENT_FEE = 8;
|
public static final Integer CHARGE_INSTALLMENT_FEE = 8;
|
||||||
private static final Integer CHARGE_OVERDUE_INSTALLMENT_FEE = 9;
|
public static final Integer CHARGE_OVERDUE_INSTALLMENT_FEE = 9;
|
||||||
private static final Integer CHARGE_OVERDRAFT_FEE = 10;
|
public static final Integer CHARGE_OVERDRAFT_FEE = 10;
|
||||||
private static final Integer WEEKLY_FEE = 11;
|
public static final Integer WEEKLY_FEE = 11;
|
||||||
private static final Integer SHAREACCOUNT_ACTIVATION = 13;
|
public static final Integer SHAREACCOUNT_ACTIVATION = 13;
|
||||||
private static final Integer SHARE_PURCHASE = 14;
|
public static final Integer SHARE_PURCHASE = 14;
|
||||||
private static final Integer SHARE_REDEEM = 15;
|
public static final Integer SHARE_REDEEM = 15;
|
||||||
|
|
||||||
private static final Integer CHARGE_SAVINGS_NO_ACTIVITY_FEE = 16;
|
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_REGULAR = 0;
|
||||||
private static final Integer CHARGE_PAYMENT_MODE_ACCOUNT_TRANSFER = 1;
|
private static final Integer CHARGE_PAYMENT_MODE_ACCOUNT_TRANSFER = 1;
|
||||||
|
|
||||||
private static final Integer CHARGE_FEE_FREQUENCY_DAYS = 0;
|
public static final Integer CHARGE_FEE_FREQUENCY_DAYS = 0;
|
||||||
private static final Integer CHARGE_FEE_FREQUENCY_WEEKS = 1;
|
public static final Integer CHARGE_FEE_FREQUENCY_WEEKS = 1;
|
||||||
private static final Integer CHARGE_FEE_FREQUENCY_MONTHS = 2;
|
public static final Integer CHARGE_FEE_FREQUENCY_MONTHS = 2;
|
||||||
private static final Integer CHARGE_FEE_FREQUENCY_YEARS = 3;
|
public static final Integer CHARGE_FEE_FREQUENCY_YEARS = 3;
|
||||||
|
|
||||||
private static final boolean ACTIVE = true;
|
private static final boolean ACTIVE = true;
|
||||||
private static final boolean PENALTY = true;
|
private static final boolean PENALTY = true;
|
||||||
@@ -491,6 +491,25 @@ public final class ChargesHelper {
|
|||||||
return 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)
|
||||||
|
@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!
|
// TODO: Rewrite to use fineract-client instead!
|
||||||
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
|
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
|
||||||
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
|
// 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