FINERACT-1971: Added reamortization foundational work
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 edbb8f8..ac4955d 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
@@ -3672,6 +3672,22 @@
return this;
}
+ public CommandWrapperBuilder reAmortize(final Long loanId) {
+ this.actionName = "REAMORTIZE";
+ this.entityName = "LOAN";
+ this.loanId = loanId;
+ this.href = "/loans/" + loanId + "/transactions?command=reAmortize";
+ return this;
+ }
+
+ public CommandWrapperBuilder undoReAmortize(final Long loanId) {
+ this.actionName = "UNDO_REAMORTIZE";
+ this.entityName = "LOAN";
+ this.loanId = loanId;
+ this.href = "/loans/" + loanId + "/transactions?command=undoReAmortize";
+ return this;
+ }
+
public CommandWrapperBuilder createDelinquencyAction(final Long loanId) {
this.actionName = "CREATE";
this.entityName = "DELINQUENCY_ACTION";
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAmortizationApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAmortizationApiConstants.java
new file mode 100644
index 0000000..5ef3744
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAmortizationApiConstants.java
@@ -0,0 +1,26 @@
+/**
+ * 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.api;
+
+public interface LoanReAmortizationApiConstants {
+
+ String localeParameterName = "locale";
+ String dateFormatParameterName = "dateFormat";
+ String externalIdParameterName = "externalId";
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
index c3985e4..603d7b9 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
@@ -57,6 +57,7 @@
private final boolean chargeoff;
private final boolean downPayment;
private final boolean reAge;
+ private final boolean reAmortize;
public LoanTransactionEnumData(final Long id, final String code, final String value) {
this.id = id;
@@ -88,6 +89,7 @@
this.chargeoff = Long.valueOf(27).equals(this.id);
this.downPayment = Long.valueOf(28).equals(this.id);
this.reAge = Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id);
+ this.reAmortize = Long.valueOf(LoanTransactionType.REAMORTIZE.getValue()).equals(this.id);
}
public boolean isRepaymentType() {
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index 811baee..7df92ed 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -685,6 +685,10 @@
return getTypeOf().isReAge() && isNotReversed();
}
+ public boolean isReAmortize() {
+ return getTypeOf().isReAmortize() && isNotReversed();
+ }
+
public boolean isIdentifiedBy(final Long identifier) {
return getId().equals(identifier);
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
index a057300..3e33114 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
@@ -61,7 +61,7 @@
CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment"), //
CHARGE_OFF(27, "loanTransactionType.chargeOff"), //
DOWN_PAYMENT(28, "loanTransactionType.downPayment"), //
- REAGE(29, "loanTransactionType.reAge");
+ REAGE(29, "loanTransactionType.reAge"), REAMORTIZE(30, "loanTransactionType.reAmortize");
private final Integer value;
private final String code;
@@ -106,6 +106,7 @@
case 27 -> LoanTransactionType.CHARGE_OFF;
case 28 -> LoanTransactionType.DOWN_PAYMENT;
case 29 -> LoanTransactionType.REAGE;
+ case 30 -> LoanTransactionType.REAMORTIZE;
default -> LoanTransactionType.INVALID;
};
}
@@ -198,6 +199,10 @@
return this.equals(LoanTransactionType.REAGE);
}
+ public boolean isReAmortize() {
+ return this.equals(LoanTransactionType.REAMORTIZE);
+ }
+
public boolean isDownPayment() {
return this.equals(LoanTransactionType.DOWN_PAYMENT);
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
index efcae19..a30de5e 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
@@ -315,6 +315,8 @@
LoanTransactionType.DOWN_PAYMENT.getCode(), "Down Payment");
case REAGE -> new LoanTransactionEnumData(LoanTransactionType.REAGE.getValue().longValue(), LoanTransactionType.REAGE.getCode(),
"Re-age");
+ case REAMORTIZE -> new LoanTransactionEnumData(LoanTransactionType.REAMORTIZE.getValue().longValue(),
+ LoanTransactionType.REAMORTIZE.getCode(), "Re-amortize");
};
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanReAmortizeTransactionBusinessEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanReAmortizeTransactionBusinessEvent.java
new file mode 100644
index 0000000..4ad0a2d
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanReAmortizeTransactionBusinessEvent.java
@@ -0,0 +1,36 @@
+/**
+ * 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.infrastructure.event.business.domain.loan.transaction.reamortization;
+
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+
+public class LoanReAmortizeTransactionBusinessEvent extends LoanTransactionBusinessEvent {
+
+ private static final String TYPE = "LoanReAmortizeTransactionBusinessEvent";
+
+ public LoanReAmortizeTransactionBusinessEvent(LoanTransaction value) {
+ super(value);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanUndoReAmortizeTransactionBusinessEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanUndoReAmortizeTransactionBusinessEvent.java
new file mode 100644
index 0000000..30cf19e
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reamortization/LoanUndoReAmortizeTransactionBusinessEvent.java
@@ -0,0 +1,36 @@
+/**
+ * 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.infrastructure.event.business.domain.loan.transaction.reamortization;
+
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+
+public class LoanUndoReAmortizeTransactionBusinessEvent extends LoanTransactionBusinessEvent {
+
+ private static final String TYPE = "LoanUndoReAmortizeTransactionBusinessEvent";
+
+ public LoanUndoReAmortizeTransactionBusinessEvent(LoanTransaction value) {
+ super(value);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
index b479448..98fd503 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
@@ -83,6 +83,8 @@
public static final String DOWN_PAYMENT = "downPayment";
public static final String UNDO_REAGE = "undoReAge";
public static final String REAGE = "reAge";
+ public static final String REAMORTIZE = "reAmortize";
+ public static final String UNDO_REAMORTIZE = "undoReAmortize";
private final Set<String> responseDataParameters = new HashSet<>(Arrays.asList("id", "type", "date", "currency", "amount", "externalId",
LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, LoanApiConstants.REVERSED_ON_DATE_PARAMNAME));
@@ -483,6 +485,10 @@
commandRequest = builder.reAge(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, UNDO_REAGE)) {
commandRequest = builder.undoReAge(resolvedLoanId).build();
+ } else if (CommandParameterUtil.is(commandParam, REAMORTIZE)) {
+ commandRequest = builder.reAmortize(resolvedLoanId).build();
+ } else if (CommandParameterUtil.is(commandParam, UNDO_REAMORTIZE)) {
+ commandRequest = builder.undoReAmortize(resolvedLoanId).build();
}
if (commandRequest == null) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanReAmortizationCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanReAmortizationCommandHandler.java
new file mode 100644
index 0000000..50dc537
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanReAmortizationCommandHandler.java
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.handler.loan.reamortization;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.portfolio.loanaccount.service.reamortization.LoanReAmortizationServiceImpl;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.orm.jpa.JpaSystemException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "LOAN", action = "REAMORTIZE")
+public class LoanReAmortizationCommandHandler implements NewCommandSourceHandler {
+
+ private final LoanReAmortizationServiceImpl loanReAmortizationService;
+ private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
+
+ @Override
+ public CommandProcessingResult processCommand(JsonCommand command) {
+ try {
+ return loanReAmortizationService.reAmortize(command.getLoanId(), command);
+ } catch (final JpaSystemException | DataIntegrityViolationException dve) {
+ dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.reAmortize",
+ "Error while handling re-amortizing");
+ return CommandProcessingResult.empty();
+ }
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanUndoReAmortizationCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanUndoReAmortizationCommandHandler.java
new file mode 100644
index 0000000..02012e1
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/loan/reamortization/LoanUndoReAmortizationCommandHandler.java
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.handler.loan.reamortization;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.portfolio.loanaccount.service.reamortization.LoanReAmortizationServiceImpl;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.orm.jpa.JpaSystemException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "LOAN", action = "UNDO_REAMORTIZE")
+public class LoanUndoReAmortizationCommandHandler implements NewCommandSourceHandler {
+
+ private final LoanReAmortizationServiceImpl loanReAmortizationService;
+ private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
+
+ @Override
+ public CommandProcessingResult processCommand(JsonCommand command) {
+ try {
+ return loanReAmortizationService.undoReAmortize(command.getLoanId(), command);
+ } catch (final JpaSystemException | DataIntegrityViolationException dve) {
+ dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.undoReAmortize",
+ "Error while handling undo re-amortizing");
+ return CommandProcessingResult.empty();
+ }
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/listener/LoanTransactionDelinquencyRecalculationListener.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/listener/LoanTransactionDelinquencyRecalculationListener.java
index 9ee55b1..2d0c27a 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/listener/LoanTransactionDelinquencyRecalculationListener.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/listener/LoanTransactionDelinquencyRecalculationListener.java
@@ -24,6 +24,8 @@
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanReAgeTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanUndoReAgeTransactionBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reamortization.LoanReAmortizeTransactionBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reamortization.LoanUndoReAmortizeTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@@ -39,7 +41,9 @@
// use-cases
private static final List<Class<? extends LoanTransactionBusinessEvent>> SUPPORTED_EVENT_TYPES = List.of(//
LoanReAgeTransactionBusinessEvent.class, //
- LoanUndoReAgeTransactionBusinessEvent.class //
+ LoanUndoReAgeTransactionBusinessEvent.class, //
+ LoanReAmortizeTransactionBusinessEvent.class, //
+ LoanUndoReAmortizeTransactionBusinessEvent.class //
);//
private final LoanAccountDomainService loanAccountDomainService;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java
new file mode 100644
index 0000000..4909adb
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java
@@ -0,0 +1,140 @@
+/**
+ * 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.reamortization;
+
+import static java.math.BigDecimal.ZERO;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reamortization.LoanReAmortizeTransactionBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reamortization.LoanUndoReAmortizeTransactionBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.portfolio.loanaccount.api.LoanReAmortizationApiConstants;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
+import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class LoanReAmortizationServiceImpl {
+
+ private final LoanAssembler loanAssembler;
+ private final LoanReAmortizationValidator reAmortizationValidator;
+ private final ExternalIdFactory externalIdFactory;
+ private final BusinessEventNotifierService businessEventNotifierService;
+ private final LoanTransactionRepository loanTransactionRepository;
+
+ public CommandProcessingResult reAmortize(Long loanId, JsonCommand command) {
+ Loan loan = loanAssembler.assembleFrom(loanId);
+ reAmortizationValidator.validateReAmortize(loan, command);
+
+ Map<String, Object> changes = new LinkedHashMap<>();
+ changes.put(LoanReAmortizationApiConstants.localeParameterName, command.locale());
+ changes.put(LoanReAmortizationApiConstants.dateFormatParameterName, command.dateFormat());
+
+ LoanTransaction reAmortizeTransaction = createReAmortizeTransaction(loan, command);
+ loanTransactionRepository.saveAndFlush(reAmortizeTransaction);
+
+ // delinquency recalculation will be triggered by the event in a decoupled way via a listener
+ businessEventNotifierService.notifyPostBusinessEvent(new LoanReAmortizeTransactionBusinessEvent(reAmortizeTransaction));
+ return new CommandProcessingResultBuilder() //
+ .withCommandId(command.commandId()) //
+ .withEntityId(reAmortizeTransaction.getId()) //
+ .withEntityExternalId(reAmortizeTransaction.getExternalId()) //
+ .withOfficeId(loan.getOfficeId()) //
+ .withClientId(loan.getClientId()) //
+ .withGroupId(loan.getGroupId()) //
+ .withLoanId(command.getLoanId()) //
+ .with(changes).build();
+ }
+
+ public CommandProcessingResult undoReAmortize(Long loanId, JsonCommand command) {
+ Loan loan = loanAssembler.assembleFrom(loanId);
+ reAmortizationValidator.validateUndoReAmortize(loan, command);
+
+ Map<String, Object> changes = new LinkedHashMap<>();
+ changes.put(LoanReAmortizationApiConstants.localeParameterName, command.locale());
+ changes.put(LoanReAmortizationApiConstants.dateFormatParameterName, command.dateFormat());
+
+ LoanTransaction reAmortizeTransaction = findLatestNonReversedReAmortizeTransaction(loan);
+ if (reAmortizeTransaction == null) {
+ // TODO: when validations implemented; throw exception if there isn't a reamortize transaction available
+ }
+ reverseReAmortizeTransaction(reAmortizeTransaction, command);
+ loanTransactionRepository.saveAndFlush(reAmortizeTransaction);
+
+ // delinquency recalculation will be triggered by the event in a decoupled way via a listener
+ businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAmortizeTransactionBusinessEvent(reAmortizeTransaction));
+ return new CommandProcessingResultBuilder() //
+ .withCommandId(command.commandId()) //
+ .withEntityId(reAmortizeTransaction.getId()) //
+ .withEntityExternalId(reAmortizeTransaction.getExternalId()) //
+ .withOfficeId(loan.getOfficeId()) //
+ .withClientId(loan.getClientId()) //
+ .withGroupId(loan.getGroupId()) //
+ .withLoanId(command.getLoanId()) //
+ .with(changes).build();
+ }
+
+ private void reverseReAmortizeTransaction(LoanTransaction reAmortizeTransaction, JsonCommand command) {
+ ExternalId reversalExternalId = externalIdFactory.createFromCommand(command,
+ LoanReAmortizationApiConstants.externalIdParameterName);
+ reAmortizeTransaction.reverse(reversalExternalId);
+ reAmortizeTransaction.manuallyAdjustedOrReversed();
+ }
+
+ private LoanTransaction findLatestNonReversedReAmortizeTransaction(Loan loan) {
+ return loan.getLoanTransactions().stream() //
+ .filter(LoanTransaction::isNotReversed) //
+ .filter(LoanTransaction::isReAmortize) //
+ .max(Comparator.comparing(LoanTransaction::getTransactionDate)) //
+ .orElse(null);
+ }
+
+ private LoanTransaction createReAmortizeTransaction(Loan loan, JsonCommand command) {
+ ExternalId txExternalId = externalIdFactory.createFromCommand(command, LoanReAmortizationApiConstants.externalIdParameterName);
+
+ // reamortize transaction date is always the current business date
+ LocalDate transactionDate = DateUtils.getBusinessLocalDate();
+
+ // in case of a reamortize transaction, only the outstanding principal amount until the business date is
+ // considered
+ Money txPrincipal = loan.getTotalPrincipalOutstandingUntil(transactionDate);
+ BigDecimal txPrincipalAmount = txPrincipal.getAmount();
+
+ return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAMORTIZE.getValue(), transactionDate, txPrincipalAmount,
+ txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, txExternalId);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java
new file mode 100644
index 0000000..643c260
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java
@@ -0,0 +1,35 @@
+/**
+ * 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.reamortization;
+
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.springframework.stereotype.Component;
+
+@Component
+public class LoanReAmortizationValidator {
+
+ public void validateReAmortize(Loan loan, JsonCommand command) {
+ // TODO: implement
+ }
+
+ public void validateUndoReAmortize(Loan loan, JsonCommand command) {
+ // TODO: implement
+ }
+}
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index 40e78b8..5b640f4 100644
--- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -156,4 +156,5 @@
<include file="parts/0134_transaction_summary_with_asset_owner_report_down_payment_amount_fix.xml" relativeToChangelogFile="true" />
<include file="parts/0135_add_external_event_for_loan_reaging.xml" relativeToChangelogFile="true" />
<include file="parts/0136_loan_reaging_parameters.xml" relativeToChangelogFile="true" />
+ <include file="parts/0137_add_external_event_for_loan_reamortization.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0137_add_external_event_for_loan_reamortization.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0137_add_external_event_for_loan_reamortization.xml
new file mode 100644
index 0000000..1579189
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0137_add_external_event_for_loan_reamortization.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ 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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+ <changeSet author="fineract" id="1">
+ <insert tableName="m_external_event_configuration">
+ <column name="type" value="LoanReAmortizeTransactionBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ </changeSet>
+ <changeSet author="fineract" id="2">
+ <insert tableName="m_external_event_configuration">
+ <column name="type" value="LoanUndoReAmortizeTransactionBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ </changeSet>
+</databaseChangeLog>
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
index 76e2256..8c30bda 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
@@ -99,7 +99,8 @@
"LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent",
"LoanTransactionDownPaymentPostBusinessEvent", "LoanTransactionDownPaymentPreBusinessEvent",
"LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent",
- "LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent");
+ "LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent",
+ "LoanUndoReAmortizeTransactionBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -180,7 +181,8 @@
"LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent",
"LoanTransactionDownPaymentPostBusinessEvent", "LoanTransactionDownPaymentPreBusinessEvent",
"LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent",
- "LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent");
+ "LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent",
+ "LoanUndoReAmortizeTransactionBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
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 375fdc2..a3073f7 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
@@ -375,10 +375,21 @@
loanTransactionHelper.reAge(loanId, request);
}
+ protected void reAmortizeLoan(Long loanId) {
+ PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
+ request.setDateFormat(DATETIME_PATTERN);
+ request.setLocale("en");
+ loanTransactionHelper.reAmortize(loanId, request);
+ }
+
protected void undoReAgeLoan(Long loanId) {
loanTransactionHelper.undoReAge(loanId, new PostLoansLoanIdTransactionsRequest());
}
+ protected void undoReAmortizeLoan(Long loanId) {
+ loanTransactionHelper.undoReAmortize(loanId, new PostLoansLoanIdTransactionsRequest());
+ }
+
protected void verifyLastClosedBusinessDate(Long loanId, String lastClosedBusinessDate) {
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertNotNull(loanDetails.getLastClosedBusinessDate());
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
index 2b8b9e1..1196b72 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
@@ -510,6 +510,16 @@
loanUndoReAgeTransactionBusinessEvent.put("enabled", false);
defaults.add(loanUndoReAgeTransactionBusinessEvent);
+ Map<String, Object> loanReAmortizeTransactionBusinessEvent = new HashMap<>();
+ loanReAmortizeTransactionBusinessEvent.put("type", "LoanReAmortizeTransactionBusinessEvent");
+ loanReAmortizeTransactionBusinessEvent.put("enabled", false);
+ defaults.add(loanReAmortizeTransactionBusinessEvent);
+
+ Map<String, Object> loanUndoReAmortizeTransactionBusinessEvent = new HashMap<>();
+ loanUndoReAmortizeTransactionBusinessEvent.put("type", "LoanUndoReAmortizeTransactionBusinessEvent");
+ loanUndoReAmortizeTransactionBusinessEvent.put("enabled", false);
+ defaults.add(loanUndoReAmortizeTransactionBusinessEvent);
+
return defaults;
}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index e61438c..9f06c03 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -593,10 +593,18 @@
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "reAge"));
}
+ public PostLoansLoanIdTransactionsResponse reAmortize(final Long loanId, final PostLoansLoanIdTransactionsRequest request) {
+ return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "reAmortize"));
+ }
+
public PostLoansLoanIdTransactionsResponse undoReAge(final Long loanId, final PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "undoReAge"));
}
+ public PostLoansLoanIdTransactionsResponse undoReAmortize(final Long loanId, final PostLoansLoanIdTransactionsRequest request) {
+ return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "undoReAmortize"));
+ }
+
public PutChargeTransactionChangesResponse undoWaiveLoanCharge(final Long loanId, final Long transactionId,
final PutChargeTransactionChangesRequest request) {
log.info("--------------------------------- UNDO WAIVE CHARGES FOR LOAN --------------------------------");
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java
new file mode 100644
index 0000000..fd39469
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java
@@ -0,0 +1,184 @@
+/**
+ * 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.reamortization;
+
+import java.math.BigDecimal;
+import java.util.concurrent.atomic.AtomicLong;
+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.integrationtests.BaseLoanIntegrationTest;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.junit.jupiter.api.Test;
+
+public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
+
+ @Test
+ public void test_LoanReAmortizeTransaction_Works() {
+ AtomicLong createdLoanId = new AtomicLong();
+
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ int numberOfRepayments = 1;
+ int repaymentEvery = 1;
+
+ // Create Loan Product
+ PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
+ .numberOfRepayments(numberOfRepayments) //
+ .repaymentEvery(repaymentEvery) //
+ .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
+
+ PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
+ Long loanProductId = loanProductResponse.getResourceId();
+
+ // Apply and Approve Loan
+ double amount = 1250.0;
+
+ PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
+ .repaymentEvery(repaymentEvery)//
+ .loanTermFrequency(numberOfRepayments)//
+ .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
+ .loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
+
+ PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
+
+ PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+ approveLoanRequest(amount, "01 January 2023"));
+
+ Long loanId = approvedLoanResult.getLoanId();
+
+ // disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(1250.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(1250.0, false, "01 February 2023") //
+ );
+
+ createdLoanId.set(loanId);
+ });
+
+ runAt("02 February 2023", () -> {
+ long loanId = createdLoanId.get();
+
+ // create re-amortize transaction
+ reAmortizeLoan(loanId);
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(1250.0, "Disbursement", "01 January 2023"), //
+ transaction(1250.0, "Re-amortize", "02 February 2023") //
+ );
+
+ // TODO: verify installments when schedule generation is implemented
+ });
+ }
+
+ @Test
+ public void test_LoanUndoReAmortizeTransaction_Works() {
+ AtomicLong createdLoanId = new AtomicLong();
+
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ int numberOfRepayments = 1;
+ int repaymentEvery = 1;
+
+ // Create Loan Product
+ PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
+ .numberOfRepayments(numberOfRepayments) //
+ .repaymentEvery(repaymentEvery) //
+ .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
+
+ PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
+ Long loanProductId = loanProductResponse.getResourceId();
+
+ // Apply and Approve Loan
+ double amount = 1250.0;
+
+ PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
+ .repaymentEvery(repaymentEvery)//
+ .loanTermFrequency(numberOfRepayments)//
+ .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
+ .loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
+
+ PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
+
+ PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+ approveLoanRequest(amount, "01 January 2023"));
+
+ Long loanId = approvedLoanResult.getLoanId();
+
+ // disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(1250.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(1250.0, false, "01 February 2023") //
+ );
+
+ createdLoanId.set(loanId);
+ });
+
+ runAt("02 February 2023", () -> {
+ long loanId = createdLoanId.get();
+
+ // create re-amortize transaction
+ reAmortizeLoan(loanId);
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(1250.0, "Disbursement", "01 January 2023"), //
+ transaction(1250.0, "Re-amortize", "02 February 2023") //
+ );
+ });
+
+ runAt("03 February 2023", () -> {
+ long loanId = createdLoanId.get();
+
+ // create re-amortize transaction
+ undoReAmortizeLoan(loanId);
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(1250.0, "Disbursement", "01 January 2023"), //
+ reversedTransaction(1250.0, "Re-amortize", "02 February 2023") //
+ );
+
+ // TODO: verify installments when schedule generation is implemented
+ });
+ }
+}