This is an automated email from the ASF dual-hosted git repository.
arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 3198b1b99e FINERACT-2208: Penalties are not recalculated after
backdated transactions
3198b1b99e is described below
commit 3198b1b99e581305b21c7fce4660d2094b9f4ed5
Author: Arnold Galovics <[email protected]>
AuthorDate: Tue Mar 11 16:11:16 2025 +0100
FINERACT-2208: Penalties are not recalculated after backdated transactions
---
.../commands/service/CommandWrapperBuilder.java | 9 +
.../infrastructure/core/service/DateUtils.java | 8 +
.../loanaccount/domain/LoanChargeRepository.java | 9 +
.../LoanChargeDeactivationException.java} | 18 +-
.../LoanChargeDeactivateOverdueCommandHandler.java | 42 +++
.../service/LoanChargeWritePlatformService.java | 2 +
.../loanaccount/api/LoanChargesApiResource.java | 5 +
.../service/LoanAccrualsProcessingServiceImpl.java | 5 +-
.../LoanChargeWritePlatformServiceImpl.java | 49 +++
.../service/LoanJournalEntryPoster.java | 54 +++
.../LoanWritePlatformServiceJpaRepositoryImpl.java | 234 ++----------
.../adjustment/LoanAdjustmentParameter.java | 25 +-
.../service/adjustment/LoanAdjustmentService.java | 19 +-
.../adjustment/LoanAdjustmentServiceImpl.java | 303 +++++++++++++++
.../starter/LoanAccountConfiguration.java | 11 +-
...est.java => LoanAdjustmentServiceImplTest.java} | 9 +-
.../integrationtests/BaseLoanIntegrationTest.java | 33 ++
.../common/charges/ChargesHelper.java | 53 ++-
.../LoanPenaltyBackdatedTransactionTest.java | 410 +++++++++++++++++++++
19 files changed, 1032 insertions(+), 266 deletions(-)
diff --git
a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index 3823aff799..d50e16a1ec 100644
---
a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++
b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -799,6 +799,15 @@ public class CommandWrapperBuilder {
return this;
}
+ public CommandWrapperBuilder deactivateOverdueLoanCharges(final Long
loanId, final Long loanChargeId) {
+ this.actionName = "DEACTIVATEOVERDUE";
+ this.entityName = "LOANCHARGE";
+ this.entityId = loanChargeId;
+ this.loanId = loanId;
+ this.href = "/loans/" + loanId + "/charges/" + loanChargeId;
+ return this;
+ }
+
public CommandWrapperBuilder deleteLoanCharge(final Long loanId, final
Long loanChargeId) {
this.actionName = "DELETE";
this.entityName = "LOANCHARGE";
diff --git
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
index abd567514d..06a5b05857 100644
---
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
@@ -302,6 +302,10 @@ public final class DateUtils {
return isAfterBusinessDate(localDate);
}
+ public static boolean isDateInThePast(final LocalDate localDate) {
+ return isBeforeBusinessDate(localDate);
+ }
+
public static int compare(LocalDate first, LocalDate second) {
return compare(first, second, true);
}
@@ -334,6 +338,10 @@ public final class DateUtils {
return first != null && (second == null || first.isAfter(second));
}
+ public static boolean isAfterInclusive(LocalDate first, LocalDate second) {
+ return isAfter(first, second) || isEqual(first, second);
+ }
+
public static long getDifference(LocalDate first, LocalDate second,
@NotNull ChronoUnit unit) {
if (first == null || second == null) {
throw new IllegalArgumentException("Dates must not be null to get
difference");
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
index e3c5ac9ed4..cc7139eede 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;
+import java.time.LocalDate;
+import java.util.List;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -30,4 +32,11 @@ public interface LoanChargeRepository extends
JpaRepository<LoanCharge, Long>, J
@Query(FIND_ID_BY_EXTERNAL_ID)
Long findIdByExternalId(@Param("externalId") ExternalId externalId);
+
+ @Query("""
+ SELECT lc FROM LoanCharge lc
+ WHERE lc.loan.id = :loanId
+ AND lc.dueDate >= :fromDate
+ """)
+ List<LoanCharge> findByLoanIdAndFromDueDate(@Param("loanId") Long loanId,
@Param("fromDate") LocalDate fromDate);
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeDeactivationException.java
similarity index 52%
copy from
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
copy to
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeDeactivationException.java
index e3c5ac9ed4..ccc9b2a056 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanChargeDeactivationException.java
@@ -16,18 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.portfolio.loanaccount.domain;
+package org.apache.fineract.portfolio.loanaccount.exception;
-import org.apache.fineract.infrastructure.core.domain.ExternalId;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
+import
org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
-public interface LoanChargeRepository extends JpaRepository<LoanCharge, Long>,
JpaSpecificationExecutor<LoanCharge> {
+public class LoanChargeDeactivationException extends
AbstractPlatformDomainRuleException {
- String FIND_ID_BY_EXTERNAL_ID = "SELECT loanCharge.id FROM LoanCharge
loanCharge WHERE loanCharge.externalId = :externalId";
-
- @Query(FIND_ID_BY_EXTERNAL_ID)
- Long findIdByExternalId(@Param("externalId") ExternalId externalId);
+ public LoanChargeDeactivationException(final String errorCode, final
String defaultUserMessage,
+ final Object... defaultUserMessageArgs) {
+ super("error.msg." + errorCode, defaultUserMessage,
defaultUserMessageArgs);
+ }
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeDeactivateOverdueCommandHandler.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeDeactivateOverdueCommandHandler.java
new file mode 100644
index 0000000000..d40fe0a114
--- /dev/null
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanChargeDeactivateOverdueCommandHandler.java
@@ -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);
+ }
+}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java
index 49ab0e9708..f96a7d60be 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java
@@ -41,5 +41,7 @@ public interface LoanChargeWritePlatformService {
CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long
loanChargeId, JsonCommand command);
+ CommandProcessingResult deactivateOverdueLoanCharge(Long loanId,
JsonCommand command);
+
void applyOverdueChargesForLoan(Long loanId,
Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList);
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java
index 379366f928..e803a5756f 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java
@@ -79,6 +79,7 @@ public class LoanChargesApiResource {
public static final String COMMAND_PAY = "pay";
public static final String COMMAND_WAIVE = "waive";
public static final String COMMAND_ADJUSTMENT = "adjustment";
+ public static final String COMMAND_DEACTIVATE_OVERDUE =
"deactivateOverdue";
private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(
Arrays.asList("id", "chargeId", "name", "penalty",
"chargeTimeType", "dueAsOfDate", "chargeCalculationType", "percentage",
"amountPercentageAppliedTo", "currency", "amountWaived",
"amountWrittenOff", "amountOutstanding", "amountOrPercentage",
@@ -463,6 +464,10 @@ public class LoanChargesApiResource {
final CommandWrapper commandRequest = new
CommandWrapperBuilder().payLoanCharge(resolvedLoanId, null)
.withJson(apiRequestBodyAsJson).build();
result =
this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
+ } else if (CommandParameterUtil.is(commandParam,
COMMAND_DEACTIVATE_OVERDUE)) {
+ final CommandWrapper commandRequest = new
CommandWrapperBuilder().deactivateOverdueLoanCharges(resolvedLoanId, null)
+ .withJson(apiRequestBodyAsJson).build();
+ result =
this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else {
final CommandWrapper commandRequest = new
CommandWrapperBuilder().createLoanCharge(resolvedLoanId)
.withJson(apiRequestBodyAsJson).build();
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
index b3f9161d74..29008d011a 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
@@ -294,6 +294,7 @@ public class LoanAccrualsProcessingServiceImpl implements
LoanAccrualsProcessing
|| !loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
return;
}
+
LoanInterestRecalculationDetails recalculationDetails =
loan.getLoanInterestRecalculationDetails();
if (recalculationDetails != null &&
recalculationDetails.isCompoundingToBePostedAsTransaction()) {
return;
@@ -523,7 +524,9 @@ public class LoanAccrualsProcessingServiceImpl implements
LoanAccrualsProcessing
loanCharges = loan.getLoanCharges(lc -> !lc.isDueAtDisbursement() &&
(lc.isInstalmentFee() ? !DateUtils.isBefore(tillDate, dueDate)
: isChargeDue(lc, tillDate, chargeOnDueDate, installment,
period.isFirstPeriod())));
for (LoanCharge loanCharge : loanCharges) {
- addChargeAccrual(loanCharge, tillDate, chargeOnDueDate,
installment, accrualPeriods);
+ if (loanCharge.isActive()) {
+ addChargeAccrual(loanCharge, tillDate, chargeOnDueDate,
installment, accrualPeriods);
+ }
}
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
index 93ab1a773a..cfb030d17e 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
@@ -121,6 +121,7 @@ import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import
org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
import
org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
import
org.apache.fineract.portfolio.loanaccount.exception.LoanChargeAdjustmentException;
+import
org.apache.fineract.portfolio.loanaccount.exception.LoanChargeDeactivationException;
import
org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
import
org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
@@ -130,6 +131,8 @@ import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDa
import
org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
import
org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import
org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
+import
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentParameter;
+import
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO;
import
org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.apache.fineract.portfolio.note.domain.Note;
@@ -174,6 +177,7 @@ public class LoanChargeWritePlatformServiceImpl implements
LoanChargeWritePlatfo
private final LoanScheduleService loanScheduleService;
private final ReprocessLoanTransactionsService
reprocessLoanTransactionsService;
private final LoanAccountService loanAccountService;
+ private final LoanAdjustmentService loanAdjustmentService;
private static boolean isPartOfThisInstallment(LoanCharge loanCharge,
LoanRepaymentScheduleInstallment e) {
return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) &&
!DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate());
@@ -776,6 +780,49 @@ public class LoanChargeWritePlatformServiceImpl implements
LoanChargeWritePlatfo
.build();
}
+ @Transactional
+ @Override
+ public CommandProcessingResult deactivateOverdueLoanCharge(Long loanId,
JsonCommand command) {
+ LocalDate fromDueDate = command.dateValueOfParameterNamed("dueDate");
+
+ List<LoanCharge> loanCharges =
loanChargeRepository.findByLoanIdAndFromDueDate(loanId, fromDueDate);
+ loanCharges.forEach(this::inactivateOverdueLoanCharge);
+
+ Loan loan = loanAssembler.assembleFrom(loanId);
+ List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments =
loan
+ .getRepaymentScheduleInstallments(si ->
DateUtils.isDateInRangeInclusive(fromDueDate, si.getFromDate(), si.getDueDate())
+ || DateUtils.isAfter(si.getFromDate(), fromDueDate));
+ repaymentScheduleInstallments.forEach(si ->
si.setPenaltyAccrued(null));
+ List<LoanTransaction> accrualsToReverse = loan.getLoanTransactions(
+ tx -> tx.isNotReversed() &&
DateUtils.isAfterInclusive(tx.getTransactionDate(), fromDueDate) &&
tx.isAccrualRelated());
+ accrualsToReverse.forEach(tx ->
loanAdjustmentService.adjustLoanTransaction(loan, tx,
+
LoanAdjustmentParameter.builder().transactionDate(tx.getTransactionDate()).build(),
null, new HashMap<>()));
+
+ loanRepositoryWrapper.saveAndFlush(loan);
+
+ final CommandProcessingResultBuilder commandProcessingResultBuilder =
new CommandProcessingResultBuilder();
+ return commandProcessingResultBuilder.withLoanId(loanId) //
+ .withEntityId(loanId) //
+ .withEntityExternalId(loan.getExternalId()) //
+ .build();
+ }
+
+ private void inactivateOverdueLoanCharge(LoanCharge loanCharge) {
+ if (!loanCharge.getChargeTimeType().isOverdueInstallment()) {
+ throw new
LoanChargeDeactivationException("loan.charge.deactivate.invalid.charge.type",
+ "Loan charge is not an overdue installment charge");
+ }
+
+ if (!loanCharge.isActive()) {
+ throw new
LoanChargeDeactivationException("loan.charge.deactivate.invalid.status", "Loan
charge is not active");
+ }
+
+ loanCharge.setActive(false);
+ loanChargeRepository.saveAndFlush(loanCharge);
+
+ businessEventNotifierService.notifyPostBusinessEvent(new
LoanUpdateChargeBusinessEvent(loanCharge));
+ }
+
@Transactional
@Override
public void applyOverdueChargesForLoan(final Long loanId,
Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList) {
@@ -802,8 +849,10 @@ public class LoanChargeWritePlatformServiceImpl implements
LoanChargeWritePlatfo
final JsonElement parsedCommand =
this.fromApiJsonHelper.parse(overdueInstallment.toString());
final JsonCommand command =
JsonCommand.from(overdueInstallment.toString(), parsedCommand,
this.fromApiJsonHelper, null, null,
null, null, null, loanId, null, null, null, null, null,
null, null, null);
+
LoanOverdueDTO overdueDTO =
applyChargeToOverdueLoanInstallment(loan, overdueInstallment.getChargeId(),
overdueInstallment.getPeriodNumber(), command);
+
loan = overdueDTO.getLoan();
runInterestRecalculation = runInterestRecalculation ||
overdueDTO.isRunInterestRecalculation();
if (DateUtils.isAfter(recalculateFrom,
overdueDTO.getRecalculateFrom())) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java
new file mode 100644
index 0000000000..b1b41291b7
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanJournalEntryPoster.java
@@ -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);
+ }
+
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
index 882578d969..f12731bc93 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
@@ -195,7 +195,6 @@ import
org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbur
import
org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
-import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
import
org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
import
org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
@@ -204,6 +203,8 @@ import
org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTr
import
org.apache.fineract.portfolio.loanaccount.serialization.LoanOfficerValidator;
import
org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
import
org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
+import
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentParameter;
+import
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import
org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
@@ -278,6 +279,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
private final LoanOfficerService loanOfficerService;
private final ReprocessLoanTransactionsService
reprocessLoanTransactionsService;
private final LoanAccountService loanAccountService;
+ private final LoanJournalEntryPoster journalEntryPoster;
+ private final LoanAdjustmentService loanAdjustmentService;
@Transactional
@Override
@@ -534,7 +537,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new
LoanDisbursalTransactionBusinessEvent(disbursalTransaction));
}
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
return new CommandProcessingResultBuilder() //
@@ -816,7 +819,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
if (!changes.isEmpty()) {
createNote(loan, command, changes);
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan,
existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
}
final Set<LoanCharge> loanCharges = loan.getActiveCharges();
@@ -1165,7 +1168,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new
LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
return new
CommandProcessingResultBuilder().withCommandId(command.commandId()) //
@@ -1332,6 +1335,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
"Interest refund transaction: " + transactionId + " cannot
be reversed or adjusted directly", transactionId);
}
+ Long commandId = command.commandId();
+ final String noteText = command.stringValueOfParameterNamed("note");
final LocalDate transactionDate =
command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount =
command.bigDecimalValueOfParameterNamed("transactionAmount");
final ExternalId txnExternalId =
externalIdFactory.createFromCommand(command,
LoanApiConstants.externalIdParameterName);
@@ -1354,181 +1359,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
changes.put("dateFormat", command.dateFormat());
changes.put("paymentTypeId",
command.longValueOfParameterNamed("paymentTypeId"));
- final List<Long> existingTransactionIds = new ArrayList<>();
- final List<Long> existingReversedTransactionIds = new ArrayList<>();
-
- final Money transactionAmountAsMoney = Money.of(loan.getCurrency(),
transactionAmount);
final PaymentDetail paymentDetail =
this.paymentDetailWritePlatformService.createPaymentDetail(command, changes);
- LoanTransaction newTransactionDetail =
LoanTransaction.repaymentType(transactionToAdjust.getTypeOf(), loan.getOffice(),
- transactionAmountAsMoney, paymentDetail, transactionDate,
txnExternalId, transactionToAdjust.getChargeRefundChargeType());
- if (transactionToAdjust.isInterestWaiver()) {
- Money unrecognizedIncome = transactionAmountAsMoney.zero();
- Money interestComponent = transactionAmountAsMoney;
- if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
- Money receivableInterest =
loan.getReceivableInterest(transactionDate);
- if
(transactionAmountAsMoney.isGreaterThan(receivableInterest)) {
- interestComponent = receivableInterest;
- unrecognizedIncome =
transactionAmountAsMoney.minus(receivableInterest);
- }
- }
- newTransactionDetail = LoanTransaction.waiver(loan.getOffice(),
loan, transactionAmountAsMoney, transactionDate,
- interestComponent, unrecognizedIncome, txnExternalId);
- }
-
- LocalDate recalculateFrom = null;
-
- if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
- recalculateFrom =
DateUtils.isAfter(transactionToAdjust.getTransactionDate(), transactionDate) ?
transactionDate
- : transactionToAdjust.getTransactionDate();
- }
-
- ScheduleGeneratorDTO scheduleGeneratorDTO =
this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
-
- HolidayDetailDTO holidayDetailDTO =
scheduleGeneratorDTO.getHolidayDetailDTO();
- if
(loan.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE))
{
- // validate cumulative
-
loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan,
transactionToAdjust.getTransactionDate(),
- LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
- }
- // common validations
-
loanTransactionValidator.validateRepaymentDateIsOnHoliday(newTransactionDetail.getTransactionDate(),
- holidayDetailDTO.isAllowTransactionsOnHoliday(),
holidayDetailDTO.getHolidays());
-
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newTransactionDetail.getTransactionDate(),
- holidayDetailDTO.getWorkingDays(),
holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
-
- adjustExistingTransaction(loan, newTransactionDetail,
loanLifecycleStateMachine, transactionToAdjust, existingTransactionIds,
- existingReversedTransactionIds, scheduleGeneratorDTO,
reversalTxnExternalId);
-
- loanAccrualsProcessingService.reprocessExistingAccruals(loan);
- if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
-
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
- }
-
- boolean thereIsNewTransaction =
newTransactionDetail.isGreaterThanZero();
- if (thereIsNewTransaction) {
- if (paymentDetail != null) {
-
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
- }
- this.loanTransactionRepository.saveAndFlush(newTransactionDetail);
- }
-
- loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
-
- final String noteText = command.stringValueOfParameterNamed("note");
- if (StringUtils.isNotBlank(noteText)) {
- changes.put("note", noteText);
- Note note;
- /**
- * If a new transaction is not created, associate note with the
transaction to be adjusted
- **/
- if (thereIsNewTransaction) {
- note = Note.loanTransactionNote(loan, newTransactionDetail,
noteText);
- } else {
- note = Note.loanTransactionNote(loan, transactionToAdjust,
noteText);
- }
- this.noteRepository.save(note);
- }
-
- Collection<Long> transactionIds = new ArrayList<>();
- List<LoanTransaction> transactions = loan.getLoanTransactions();
- for (LoanTransaction transaction : transactions) {
- if (transaction.isRefund() && transaction.isNotReversed()) {
- transactionIds.add(transaction.getId());
- }
- }
-
- if (!transactionIds.isEmpty()) {
-
this.accountTransfersWritePlatformService.reverseTransfersWithFromAccountTransactions(transactionIds,
- PortfolioAccountType.LOAN);
- loan.updateLoanSummaryAndStatus();
- }
-
-
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.isInterestBearingAndInterestRecalculationEnabled(),
- false);
-
- this.loanAccountDomainService.setLoanDelinquencyTag(loan,
DateUtils.getBusinessLocalDate());
- LoanAdjustTransactionBusinessEvent.Data eventData = new
LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust);
- if (newTransactionDetail.isRepaymentLikeType() &&
thereIsNewTransaction) {
- eventData.setNewTransactionDetail(newTransactionDetail);
- }
- Long entityId = transactionToAdjust.getId();
- ExternalId entityExternalId = transactionToAdjust.getExternalId();
+ LoanAdjustmentParameter parameter =
LoanAdjustmentParameter.builder().transactionAmount(transactionAmount)
+
.paymentDetail(paymentDetail).transactionDate(transactionDate).txnExternalId(txnExternalId)
+
.reversalTxnExternalId(reversalTxnExternalId).noteText(noteText).build();
- if (thereIsNewTransaction) {
- entityId = newTransactionDetail.getId();
- entityExternalId = newTransactionDetail.getExternalId();
- }
- businessEventNotifierService.notifyPostBusinessEvent(new
LoanBalanceChangedBusinessEvent(loan));
- businessEventNotifierService.notifyPostBusinessEvent(new
LoanAdjustTransactionBusinessEvent(eventData));
-
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
-
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
-
- return new CommandProcessingResultBuilder() //
- .withCommandId(command.commandId()) //
- .withEntityId(entityId) //
- .withEntityExternalId(entityExternalId) //
- .withOfficeId(loan.getOfficeId()) //
- .withClientId(loan.getClientId()) //
- .withGroupId(loan.getGroupId()) //
- .withLoanId(loanId) //
- .with(changes).build();
- }
-
- public void adjustExistingTransaction(final Loan loan, final
LoanTransaction newTransactionDetail,
- final LoanLifecycleStateMachine loanLifecycleStateMachine, final
LoanTransaction transactionForAdjustment,
- final List<Long> existingTransactionIds, final List<Long>
existingReversedTransactionIds,
- final ScheduleGeneratorDTO scheduleGeneratorDTO, final ExternalId
reversalExternalId) {
- existingTransactionIds.addAll(loan.findExistingTransactionIds());
-
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
-
-
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan,
LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
- transactionForAdjustment.getTransactionDate());
-
- if (transactionForAdjustment.isNotRepaymentLikeType() &&
transactionForAdjustment.isNotWaiver()
- && transactionForAdjustment.isNotCreditBalanceRefund()) {
- final String errorMessage = "Only (non-reversed) transactions of
type repayment, waiver or credit balance refund can be adjusted.";
- throw new InvalidLoanTransactionTypeException("transaction",
-
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions",
errorMessage);
- }
-
-
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(transactionForAdjustment.getLoan(),
- transactionForAdjustment, "reversed");
- transactionForAdjustment.reverse(reversalExternalId);
- transactionForAdjustment.manuallyAdjustedOrReversed();
-
- if
(transactionForAdjustment.getTypeOf().equals(LoanTransactionType.MERCHANT_ISSUED_REFUND)
- ||
transactionForAdjustment.getTypeOf().equals(LoanTransactionType.PAYOUT_REFUND))
{
- loan.getLoanTransactions().stream() //
- .filter(LoanTransaction::isNotReversed)
- .filter(loanTransaction ->
loanTransaction.getLoanTransactionRelations().stream()
- .anyMatch(relation ->
relation.getRelationType().equals(LoanTransactionRelationTypeEnum.RELATED)
- &&
relation.getToTransaction().getId().equals(transactionForAdjustment.getId())))
- .forEach(loanTransaction -> {
-
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(),
- loanTransaction, "reversed");
- loanTransaction.reverse();
- loanTransaction.manuallyAdjustedOrReversed();
- LoanAdjustTransactionBusinessEvent.Data eventData =
new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
-
businessEventNotifierService.notifyPostBusinessEvent(new
LoanAdjustTransactionBusinessEvent(eventData));
- });
- }
-
- if (loan.isClosedWrittenOff()) {
- // find write off transaction and reverse it
- final LoanTransaction writeOffTransaction =
loan.findWriteOffTransaction();
-
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(writeOffTransaction.getLoan(),
writeOffTransaction,
- "reversed");
- writeOffTransaction.reverse();
- }
-
- loan.updateLoanSummaryAndStatus();
-
- if (newTransactionDetail.isRepaymentLikeType() ||
newTransactionDetail.isInterestWaiver()) {
-
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan,
newTransactionDetail,
- loanLifecycleStateMachine, transactionForAdjustment,
scheduleGeneratorDTO);
- }
+ return loanAdjustmentService.adjustLoanTransaction(loan,
transactionToAdjust, parameter, commandId, changes);
}
@Transactional
@@ -1611,7 +1448,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
this.noteRepository.save(note);
}
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
this.loanAccountDomainService.setLoanDelinquencyTag(loan,
transactionDate);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(new
LoanChargebackTransactionBusinessEvent(newTransaction));
@@ -1714,7 +1551,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new
LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new
LoanWaiveInterestBusinessEvent(waiveInterestTransaction));
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
@@ -1808,7 +1645,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new
LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new
LoanWrittenOffPostBusinessEvent(loanTransaction));
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan,
existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
builder.withEntityId(loanTransaction.getId()).withEntityExternalId(loanTransaction.getExternalId());
@@ -1885,7 +1722,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
// disable all active standing instructions linked to the loan
this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan);
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
CommandProcessingResult result;
@@ -2021,7 +1858,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
this.loanTransactionRepository.saveAndFlush(newTransferTransaction);
saveLoanWithDataIntegrityViolationChecks(loan);
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(new
LoanInitiateTransferBusinessEvent(loan));
return newTransferTransaction;
}
@@ -2051,7 +1888,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
saveLoanWithDataIntegrityViolationChecks(loan);
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(new
LoanAcceptTransferBusinessEvent(loan));
return newTransferAcceptanceTransaction;
@@ -2077,7 +1914,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
saveLoanWithDataIntegrityViolationChecks(loan);
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(new
LoanWithdrawTransferBusinessEvent(loan));
return newTransferAcceptanceTransaction;
@@ -2209,25 +2046,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
.build();
}
- private void postJournalEntries(final Loan loan, final List<Long>
existingTransactionIds,
- final List<Long> existingReversedTransactionIds) {
-
- final MonetaryCurrency currency = loan.getCurrency();
- boolean isAccountTransfer = false;
- List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
- if (loan.isChargedOff()) {
- accountingBridgeData =
loan.deriveAccountingBridgeDataForChargeOff(currency.getCode(),
existingTransactionIds,
- existingReversedTransactionIds, isAccountTransfer);
- } else {
-
accountingBridgeData.add(loan.deriveAccountingBridgeData(currency.getCode(),
existingTransactionIds,
- existingReversedTransactionIds, isAccountTransfer));
- }
- for (Map<String, Object> accountingData : accountingBridgeData) {
-
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingData);
- }
-
- }
-
@Transactional
@Override
public void applyMeetingDateChanges(final Calendar calendar, final
Collection<CalendarInstance> loanCalendarInstances) {
@@ -2503,7 +2321,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
this.loanAccountDomainService.setLoanDelinquencyTag(loan,
DateUtils.getBusinessLocalDate());
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
return new CommandProcessingResultBuilder() //
@@ -2647,7 +2465,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
false);
this.loanAccountDomainService.setLoanDelinquencyTag(loan,
DateUtils.getBusinessLocalDate());
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
return new CommandProcessingResultBuilder() //
@@ -2709,7 +2527,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
false);
businessEventNotifierService.notifyPostBusinessEvent(new
LoanInterestRecalculationBusinessEvent(loan));
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
return loan;
}
@@ -3129,7 +2947,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new
LoanAdjustTransactionBusinessEvent(data));
});
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(new
LoanChargeOffPostBusinessEvent(chargeOffTransaction));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
@@ -3192,7 +3010,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
reprocessLoanTransactionsService.reprocessTransactions(loan);
saveLoanWithDataIntegrityViolationChecks(loan);
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(new
LoanUndoChargeOffBusinessEvent(chargedOffTransaction));
return new CommandProcessingResultBuilder() //
@@ -3267,7 +3085,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new
LoanBalanceChangedBusinessEvent(loan));
// Create journal entries for the new transaction(s)
- postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
+ journalEntryPoster.postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
Long entityId = refundTransaction.getId();
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentParameter.java
similarity index 57%
copy from
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
copy to
fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentParameter.java
index e3c5ac9ed4..b6dca479ee 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentParameter.java
@@ -16,18 +16,23 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.portfolio.loanaccount.domain;
+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.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
+import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
-public interface LoanChargeRepository extends JpaRepository<LoanCharge, Long>,
JpaSpecificationExecutor<LoanCharge> {
+@Data
+@Builder
+public class LoanAdjustmentParameter {
- String FIND_ID_BY_EXTERNAL_ID = "SELECT loanCharge.id FROM LoanCharge
loanCharge WHERE loanCharge.externalId = :externalId";
-
- @Query(FIND_ID_BY_EXTERNAL_ID)
- Long findIdByExternalId(@Param("externalId") ExternalId externalId);
+ private BigDecimal transactionAmount;
+ private PaymentDetail paymentDetail;
+ private LocalDate transactionDate;
+ private ExternalId txnExternalId;
+ private ExternalId reversalTxnExternalId;
+ private String noteText;
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentService.java
similarity index 52%
copy from
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
copy to
fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentService.java
index e3c5ac9ed4..884b7a3016 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentService.java
@@ -16,18 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.portfolio.loanaccount.domain;
+package org.apache.fineract.portfolio.loanaccount.service.adjustment;
-import org.apache.fineract.infrastructure.core.domain.ExternalId;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
-import org.springframework.data.jpa.repository.Query;
-import org.springframework.data.repository.query.Param;
+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 LoanChargeRepository extends JpaRepository<LoanCharge, Long>,
JpaSpecificationExecutor<LoanCharge> {
+public interface LoanAdjustmentService {
- String FIND_ID_BY_EXTERNAL_ID = "SELECT loanCharge.id FROM LoanCharge
loanCharge WHERE loanCharge.externalId = :externalId";
-
- @Query(FIND_ID_BY_EXTERNAL_ID)
- Long findIdByExternalId(@Param("externalId") ExternalId externalId);
+ CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction
transactionToAdjust, LoanAdjustmentParameter parameter,
+ Long commandId, Map<String, Object> changes);
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java
new file mode 100644
index 0000000000..a57a3954c2
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java
@@ -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;
+ }
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
index dcf9cdb646..b4aa6029ab 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
@@ -127,6 +127,7 @@ import
org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetails
import
org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementService;
import
org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
import
org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerServiceImpl;
+import
org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
import
org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import
org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl;
@@ -142,6 +143,7 @@ import
org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServic
import
org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
import
org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl;
import
org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
+import
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import
org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
import
org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
@@ -296,7 +298,7 @@ public class LoanAccountConfiguration {
LoanAccrualsProcessingService loanAccrualsProcessingService,
LoanDownPaymentTransactionValidator
loanDownPaymentTransactionValidator, LoanChargeValidator loanChargeValidator,
LoanScheduleService loanScheduleService,
ReprocessLoanTransactionsService reprocessLoanTransactionsService,
- LoanAccountService loanAccountService) {
+ LoanAccountService loanAccountService, LoanAdjustmentService
loanAdjustmentService) {
return new
LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler,
chargeRepository,
businessEventNotifierService, loanTransactionRepository,
accountTransfersWritePlatformService, loanRepositoryWrapper,
journalEntryWritePlatformService, loanAccountDomainService,
loanChargeRepository, loanWritePlatformService, loanUtilService,
@@ -304,7 +306,7 @@ public class LoanAccountConfiguration {
configurationDomainService,
loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory,
accountTransferDetailRepository, loanChargeAssembler,
paymentDetailWritePlatformService, noteRepository,
loanAccrualTransactionBusinessEventService,
loanAccrualsProcessingService, loanDownPaymentTransactionValidator,
- loanChargeValidator, loanScheduleService,
reprocessLoanTransactionsService, loanAccountService);
+ loanChargeValidator, loanScheduleService,
reprocessLoanTransactionsService, loanAccountService, loanAdjustmentService);
}
@Bean
@@ -392,7 +394,8 @@ public class LoanAccountConfiguration {
LoanOfficerValidator loanOfficerValidator,
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator,
LoanDisbursementService loanDisbursementService,
LoanScheduleService loanScheduleService,
LoanChargeValidator loanChargeValidator, LoanOfficerService
loanOfficerService,
- ReprocessLoanTransactionsService reprocessLoanTransactionsService,
LoanAccountService loanAccountService) {
+ ReprocessLoanTransactionsService reprocessLoanTransactionsService,
LoanAccountService loanAccountService,
+ LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService
loanAdjustmentService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(context,
loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanRepositoryWrapper, loanAccountDomainService,
noteRepository, loanTransactionRepository,
loanTransactionRelationRepository, loanAssembler,
journalEntryWritePlatformService, calendarInstanceRepository,
@@ -406,7 +409,7 @@ public class LoanAccountConfiguration {
loanAccountLockService, externalIdFactory,
loanAccrualTransactionBusinessEventService, errorHandler,
loanDownPaymentHandlerService, loanTransactionAssembler,
loanAccrualsProcessingService, loanOfficerValidator,
loanDownPaymentTransactionValidator, loanDisbursementService,
loanScheduleService, loanChargeValidator, loanOfficerService,
- reprocessLoanTransactionsService, loanAccountService);
+ reprocessLoanTransactionsService, loanAccountService,
journalEntryPoster, loanAdjustmentService);
}
@Bean
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAdjustmentServiceImplTest.java
similarity index 97%
rename from
fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java
rename to
fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAdjustmentServiceImplTest.java
index a4fc70a01c..16588e705c 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAdjustmentServiceImplTest.java
@@ -78,6 +78,7 @@ import
org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationVa
import
org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import
org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
import
org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
+import
org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentServiceImpl;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import
org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import
org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository;
@@ -90,10 +91,10 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
-class LoanWritePlatformServiceJpaRepositoryImplTest {
+class LoanAdjustmentServiceImplTest {
@InjectMocks
- private LoanWritePlatformServiceJpaRepositoryImpl loanWritePlatformService;
+ private LoanAdjustmentServiceImpl underTest;
@Mock
private LoanRepaymentScheduleTransactionProcessorFactory
transactionProcessorFactory;
@@ -240,7 +241,7 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
// Act
- loanWritePlatformService.adjustExistingTransaction(loan,
newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
+ underTest.adjustExistingTransaction(loan, newTransactionDetail,
loanLifecycleStateMachine, transactionForAdjustment,
existingTransactionIds, existingReversedTransactionIds,
scheduleGeneratorDTO, reversalExternalId);
// Assert
@@ -285,7 +286,7 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
// Act
- loanWritePlatformService.adjustExistingTransaction(loan,
newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment,
+ underTest.adjustExistingTransaction(loan, newTransactionDetail,
loanLifecycleStateMachine, transactionForAdjustment,
existingTransactionIds, existingReversedTransactionIds,
scheduleGeneratorDTO, reversalExternalId);
// Assert
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index 414180cc4e..372607070e 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -61,6 +61,7 @@ import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.AllowAttributeOverrides;
import org.apache.fineract.client.models.BusinessDateRequest;
import
org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdChargesChargeIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdStatus;
@@ -71,6 +72,7 @@ import org.apache.fineract.client.models.LoanPointInTimeData;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostChargesResponse;
import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest;
import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse;
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
@@ -796,6 +798,14 @@ public abstract class BaseLoanIntegrationTest extends
IntegrationTest {
return chargeId.longValue();
}
+ protected Long createOverduePenaltyPercentageCharge(double
percentageAmount, Integer feeFrequency, int feeInterval) {
+ Integer chargeId = ChargesHelper.createCharges(requestSpec,
responseSpec,
+
ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentageWithFeeInterval(String.valueOf(percentageAmount),
+ feeFrequency, feeInterval));
+ assertNotNull(chargeId);
+ return chargeId.longValue();
+ }
+
protected void verifyRepaymentSchedule(GetLoansLoanIdResponse
savedLoanResponse, GetLoansLoanIdResponse actualLoanResponse,
int totalPeriods, int identicalPeriods) {
List<GetLoansLoanIdRepaymentPeriod> savedPeriods =
savedLoanResponse.getRepaymentSchedule().getPeriods();
@@ -934,6 +944,18 @@ public abstract class BaseLoanIntegrationTest extends
IntegrationTest {
}
}
+ protected void runFromToInclusive(String fromDate, String toDate, Runnable
runnable) {
+ DateTimeFormatter format =
DateTimeFormatter.ofPattern(DATETIME_PATTERN);
+ LocalDate startDate = LocalDate.parse(fromDate, format);
+ LocalDate endDate = LocalDate.parse(toDate, format);
+
+ LocalDate currentDate = startDate;
+ while (currentDate.isBefore(endDate) || currentDate.isEqual(endDate)) {
+ runAt(format.format(currentDate), runnable);
+ currentDate = currentDate.plusDays(1);
+ }
+ }
+
protected void runAt(String date, Runnable runnable) {
try {
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.ENABLE_BUSINESS_DATE,
@@ -1092,6 +1114,17 @@ public abstract class BaseLoanIntegrationTest extends
IntegrationTest {
return loanTransactionHelper.addChargeForLoan(loanId.intValue(),
payload, responseSpec);
}
+ protected List<GetLoansLoanIdChargesChargeIdResponse>
getOverdueInstallmentLoanCharges(Long loanId) {
+ return
ok(fineractClient().loanCharges.retrieveAllLoanCharges(loanId)).stream() //
+ .filter(ch -> ch.getChargeTimeType().getId().intValue() ==
ChargesHelper.CHARGE_OVERDUE_INSTALLMENT_FEE) //
+ .toList(); //
+ }
+
+ protected void deactivateOverdueLoanCharges(Long loanId, String
fromDueDate) {
+ ok(fineractClient().loanCharges.executeLoanCharge(loanId,
+ new
PostLoansLoanIdChargesRequest().dueDate(fromDueDate).dateFormat(DATETIME_PATTERN).locale("en"),
"deactivateOverdue"));
+ }
+
protected void waiveLoanCharge(Long loanId, Long chargeId, Integer
installmentNumber) {
String payload =
LoanTransactionHelper.getWaiveChargeJSON(installmentNumber.toString());
loanTransactionHelper.waiveChargesForLoan(loanId.intValue(),
chargeId.intValue(), payload);
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java
index 36707f9091..d27171b306 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java
@@ -50,19 +50,19 @@ public final class ChargesHelper {
private static final Integer CHARGE_APPLIES_TO_CLIENT = 3;
private static final Integer CHARGE_APPLIES_TO_SHARES = 4;
- private static final Integer CHARGE_DISBURSEMENT_FEE = 1;
- private static final Integer CHARGE_SPECIFIED_DUE_DATE = 2;
- private static final Integer CHARGE_SAVINGS_ACTIVATION_FEE = 3;
- private static final Integer CHARGE_WITHDRAWAL_FEE = 5;
- private static final Integer CHARGE_ANNUAL_FEE = 6;
- private static final Integer CHARGE_MONTHLY_FEE = 7;
- private static final Integer CHARGE_INSTALLMENT_FEE = 8;
- private static final Integer CHARGE_OVERDUE_INSTALLMENT_FEE = 9;
- private static final Integer CHARGE_OVERDRAFT_FEE = 10;
- private static final Integer WEEKLY_FEE = 11;
- private static final Integer SHAREACCOUNT_ACTIVATION = 13;
- private static final Integer SHARE_PURCHASE = 14;
- private static final Integer SHARE_REDEEM = 15;
+ public static final Integer CHARGE_DISBURSEMENT_FEE = 1;
+ public static final Integer CHARGE_SPECIFIED_DUE_DATE = 2;
+ public static final Integer CHARGE_SAVINGS_ACTIVATION_FEE = 3;
+ public static final Integer CHARGE_WITHDRAWAL_FEE = 5;
+ public static final Integer CHARGE_ANNUAL_FEE = 6;
+ public static final Integer CHARGE_MONTHLY_FEE = 7;
+ public static final Integer CHARGE_INSTALLMENT_FEE = 8;
+ public static final Integer CHARGE_OVERDUE_INSTALLMENT_FEE = 9;
+ public static final Integer CHARGE_OVERDRAFT_FEE = 10;
+ public static final Integer WEEKLY_FEE = 11;
+ public static final Integer SHAREACCOUNT_ACTIVATION = 13;
+ public static final Integer SHARE_PURCHASE = 14;
+ public static final Integer SHARE_REDEEM = 15;
private static final Integer CHARGE_SAVINGS_NO_ACTIVITY_FEE = 16;
@@ -76,10 +76,10 @@ public final class ChargesHelper {
private static final Integer CHARGE_PAYMENT_MODE_REGULAR = 0;
private static final Integer CHARGE_PAYMENT_MODE_ACCOUNT_TRANSFER = 1;
- private static final Integer CHARGE_FEE_FREQUENCY_DAYS = 0;
- private static final Integer CHARGE_FEE_FREQUENCY_WEEKS = 1;
- private static final Integer CHARGE_FEE_FREQUENCY_MONTHS = 2;
- private static final Integer CHARGE_FEE_FREQUENCY_YEARS = 3;
+ public static final Integer CHARGE_FEE_FREQUENCY_DAYS = 0;
+ public static final Integer CHARGE_FEE_FREQUENCY_WEEKS = 1;
+ public static final Integer CHARGE_FEE_FREQUENCY_MONTHS = 2;
+ public static final Integer CHARGE_FEE_FREQUENCY_YEARS = 3;
private static final boolean ACTIVE = true;
private static final boolean PENALTY = true;
@@ -491,6 +491,25 @@ public final class ChargesHelper {
return chargesCreateJson;
}
+ // TODO: Rewrite to use fineract-client instead!
+ // Example:
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
+ // org.apache.fineract.client.models.PostLoansLoanIdRequest)
+ @Deprecated(forRemoval = true)
+ public static String
getLoanOverdueFeeJSONWithCalculationTypePercentageWithFeeInterval(String
penaltyPercentageAmount,
+ Integer feeFrequency, int feeInterval) {
+ final HashMap<String, Object> map = populateDefaultsForLoan();
+ map.put("penalty", ChargesHelper.PENALTY);
+ map.put("amount", penaltyPercentageAmount);
+ map.put("chargePaymentMode",
ChargesHelper.CHARGE_PAYMENT_MODE_REGULAR);
+ map.put("chargeTimeType", CHARGE_OVERDUE_INSTALLMENT_FEE);
+ map.put("chargeCalculationType",
ChargesHelper.CHARGE_CALCULATION_TYPE_PERCENTAGE_AMOUNT_AND_INTEREST);
+ map.put("feeFrequency", feeFrequency);
+ map.put("feeInterval", feeInterval);
+ String chargesCreateJson = new Gson().toJson(map);
+ LOG.info("{}", chargesCreateJson);
+ return chargesCreateJson;
+ }
+
// TODO: Rewrite to use fineract-client instead!
// Example:
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/penalty/LoanPenaltyBackdatedTransactionTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/penalty/LoanPenaltyBackdatedTransactionTest.java
new file mode 100644
index 0000000000..6c9cd69752
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/penalty/LoanPenaltyBackdatedTransactionTest.java
@@ -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) //
+ );
+ });
+ }
+}