You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ad...@apache.org on 2024/06/21 09:40:26 UTC
(fineract) branch develop updated: FINERACT-2082: New Transaction Type - Interest Payment Waiver
This is an automated email from the ASF dual-hosted git repository.
adamsaghy 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 9152408c2 FINERACT-2082: New Transaction Type - Interest Payment Waiver
9152408c2 is described below
commit 9152408c241c1df62f77bed71cbff30370e107b3
Author: Soma Sörös <so...@dpc.hu>
AuthorDate: Wed Jun 19 10:51:03 2024 +0200
FINERACT-2082: New Transaction Type - Interest Payment Waiver
---
.../commands/service/CommandWrapperBuilder.java | 9 +
...tionInterestPaymentWaiverPostBusinessEvent.java | 35 +
...ctionInterestPaymentWaiverPreBusinessEvent.java | 36 +
.../loanaccount/data/LoanTransactionEnumData.java | 2 +
.../portfolio/loanaccount/domain/Loan.java | 21 +-
.../loanaccount/domain/LoanTransaction.java | 9 +-
.../loanaccount/domain/LoanTransactionType.java | 9 +-
.../service/LoanWritePlatformService.java | 3 +
.../domain/PaymentAllocationTransactionType.java | 3 +-
.../loanproduct/service/LoanEnumerations.java | 2 +
...dvancedPaymentScheduleTransactionProcessor.java | 2 +-
.../AccrualBasedAccountingProcessorForLoan.java | 100 ++
.../api/LoanTransactionsApiResource.java | 18 +-
.../domain/LoanAccountDomainServiceJpa.java | 6 +
.../LoanInterestPaymentWaiverCommandHandler.java | 44 +
...alidator.java => LoanTransactionValidator.java} | 174 +-
.../service/LoanTransactionAssembler.java | 68 +
.../loanaccount/service/LoanUtilService.java | 11 +
.../LoanWritePlatformServiceJpaRepositoryImpl.java | 347 +++-
.../starter/LoanAccountConfiguration.java | 22 +-
.../db/changelog/tenant/changelog-tenant.xml | 1 +
...dd_interest_payment_waiver_transaction_type.xml | 49 +
...nalEventConfigurationValidationServiceTest.java | 6 +-
.../integrationtests/BaseLoanIntegrationTest.java | 9 +
...hAdvancedPaymentAllocationIntegrationTests.java | 69 +
.../LoanTransactionInterestPaymentWaiverTest.java | 1767 ++++++++++++++++++++
.../integrationtests/common/BatchHelper.java | 17 +
.../common/ExternalEventConfigurationHelper.java | 10 +
.../common/loans/LoanTransactionHelper.java | 10 +
29 files changed, 2809 insertions(+), 50 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 8d5ddd298..dad66a861 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
@@ -842,6 +842,15 @@ public class CommandWrapperBuilder {
return this;
}
+ public CommandWrapperBuilder loanInterestPaymentWaiverTransaction(final Long loanId) {
+ this.actionName = "INTERESTPAYMENTWAIVER";
+ this.entityName = "LOAN";
+ this.entityId = null;
+ this.loanId = loanId;
+ this.href = "/loans/" + loanId + "/transactions/template?command=interestpaymentwaiver";
+ return this;
+ }
+
public CommandWrapperBuilder refundLoanCharge(final Long loanId) {
this.actionName = "CHARGEREFUND";
this.entityName = "LOAN";
diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionInterestPaymentWaiverPostBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionInterestPaymentWaiverPostBusinessEvent.java
new file mode 100644
index 000000000..23d5cc35a
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionInterestPaymentWaiverPostBusinessEvent.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.infrastructure.event.business.domain.loan.transaction;
+
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+
+public class LoanTransactionInterestPaymentWaiverPostBusinessEvent extends LoanTransactionBusinessEvent {
+
+ private static final String TYPE = "LoanTransactionInterestPaymentWaiverPostBusinessEvent";
+
+ public LoanTransactionInterestPaymentWaiverPostBusinessEvent(LoanTransaction value) {
+ super(value);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionInterestPaymentWaiverPreBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionInterestPaymentWaiverPreBusinessEvent.java
new file mode 100644
index 000000000..361ac54ae
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionInterestPaymentWaiverPreBusinessEvent.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;
+
+import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusinessEvent;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+
+public class LoanTransactionInterestPaymentWaiverPreBusinessEvent extends LoanBusinessEvent {
+
+ private static final String TYPE = "LoanTransactionInterestPaymentWaiverPreBusinessEvent";
+
+ public LoanTransactionInterestPaymentWaiverPreBusinessEvent(Loan value) {
+ super(value);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+}
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 603d7b989..9d5c292b7 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
@@ -37,6 +37,7 @@ public class LoanTransactionEnumData {
private final boolean merchantIssuedRefund;
private final boolean payoutRefund;
private final boolean goodwillCredit;
+ private final boolean interestPaymentWaiver;
private final boolean chargeRefund;
private final boolean contra;
private final boolean waiveInterest;
@@ -88,6 +89,7 @@ public class LoanTransactionEnumData {
this.chargeAdjustment = Long.valueOf(26).equals(this.id);
this.chargeoff = Long.valueOf(27).equals(this.id);
this.downPayment = Long.valueOf(28).equals(this.id);
+ this.interestPaymentWaiver = Long.valueOf(31).equals(this.id);
this.reAge = Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id);
this.reAmortize = Long.valueOf(LoanTransactionType.REAMORTIZE.getValue()).equals(this.id);
}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index f8e960e39..a39fd9ebc 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -1387,7 +1387,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
}
- private void updateLoanSummaryDerivedFields() {
+ public void updateLoanSummaryDerivedFields() {
if (isNotDisbursed()) {
this.summary.zeroFields();
this.totalOverpaid = null;
@@ -2586,9 +2586,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransaction, LoanEvent event) {
- if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isMerchantIssuedRefund()
- || repaymentTransaction.isPayoutRefund() || repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment()
- || repaymentTransaction.isDownPayment()) {
+ if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isInterestPaymentWaiver()
+ || repaymentTransaction.isMerchantIssuedRefund() || repaymentTransaction.isPayoutRefund()
+ || repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment() || repaymentTransaction.isDownPayment()) {
+
if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid";
@@ -2799,7 +2800,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return loanTransactionDate;
}
- private LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate dueDate) {
+ public LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate dueDate) {
return getRepaymentScheduleInstallments().stream() //
.filter(installment -> dueDate.equals(installment.getDueDate())).findFirst() //
.orElse(null);
@@ -2887,7 +2888,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
processIncomeAccrualTransactionOnLoanClosure();
}
- private void processIncomeAccrualTransactionOnLoanClosure() {
+ public void processIncomeAccrualTransactionOnLoanClosure() {
if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()
&& this.getStatus().isClosedObligationsMet() && !isNpa() && !isChargedOff()) {
@@ -3161,7 +3162,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.orElse(null);
}
- private boolean isOverPaid() {
+ public boolean isOverPaid() {
return calculateTotalOverpayment().isGreaterThanZero();
}
@@ -3209,7 +3210,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return getTotalRecoveredPayments();
}
- private MonetaryCurrency loanCurrency() {
+ public MonetaryCurrency loanCurrency() {
return this.loanRepaymentScheduleDetail.getCurrency();
}
@@ -3479,7 +3480,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return getStatus().isClosed() || isCancelled();
}
- private boolean isClosedObligationsMet() {
+ public boolean isClosedObligationsMet() {
return getStatus().isClosedObligationsMet();
}
@@ -6024,7 +6025,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
}
- private boolean isForeclosure() {
+ public boolean isForeclosure() {
boolean isForeClosure = false;
if (this.loanSubStatus != null) {
isForeClosure = LoanSubStatus.fromInt(loanSubStatus).isForeclosed();
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 789407016..1508b4829 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
@@ -580,11 +580,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
public boolean isRepaymentLikeType() {
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()
- || isChargeAdjustment() || isDownPayment();
+ || isChargeAdjustment() || isDownPayment() || isInterestPaymentWaiver();
}
public boolean isTypeAllowedForChargeback() {
- return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isDownPayment();
+ return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isDownPayment()
+ || isInterestPaymentWaiver();
}
public boolean isRepayment() {
@@ -607,6 +608,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
return LoanTransactionType.GOODWILL_CREDIT.equals(getTypeOf()) && isNotReversed();
}
+ public boolean isInterestPaymentWaiver() {
+ return LoanTransactionType.INTEREST_PAYMENT_WAIVER.equals(getTypeOf()) && isNotReversed();
+ }
+
public boolean isChargeRefund() {
return LoanTransactionType.CHARGE_REFUND.equals(getTypeOf()) && isNotReversed();
}
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 4a02782b6..f78c1d67a 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
@@ -63,6 +63,7 @@ public enum LoanTransactionType {
DOWN_PAYMENT(28, "loanTransactionType.downPayment"), //
REAGE(29, "loanTransactionType.reAge"), //
REAMORTIZE(30, "loanTransactionType.reAmortize"), //
+ INTEREST_PAYMENT_WAIVER(31, "loanTransactionType.interestPaymentWaiver"), //
;
private final Integer value;
@@ -109,6 +110,7 @@ public enum LoanTransactionType {
case 28 -> LoanTransactionType.DOWN_PAYMENT;
case 29 -> LoanTransactionType.REAGE;
case 30 -> LoanTransactionType.REAMORTIZE;
+ case 31 -> LoanTransactionType.INTEREST_PAYMENT_WAIVER;
default -> LoanTransactionType.INVALID;
};
}
@@ -133,6 +135,10 @@ public enum LoanTransactionType {
return this.equals(LoanTransactionType.REPAYMENT);
}
+ public boolean isInterestPaymentWaiver() {
+ return this.equals(LoanTransactionType.INTEREST_PAYMENT_WAIVER);
+ }
+
public boolean isMerchantIssuedRefund() {
return this.equals(LoanTransactionType.MERCHANT_ISSUED_REFUND);
}
@@ -150,7 +156,8 @@ public enum LoanTransactionType {
}
public boolean isRepaymentType() {
- return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() || isDownPayment());
+ return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() || isDownPayment()
+ || isInterestPaymentWaiver());
}
public boolean isRecoveryRepayment() {
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java
index c3f3e4166..bba061edc 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java
@@ -52,6 +52,9 @@ public interface LoanWritePlatformService {
CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType repaymentTransactionType, Long loanId,
JsonCommand command, boolean isRecoveryRepayment, String chargeRefundChargeType);
+ @Transactional
+ CommandProcessingResult makeInterestPaymentWaiver(JsonCommand command);
+
Map<String, Object> makeLoanBulkRepayment(CollectionSheetBulkRepaymentCommand bulkRepaymentCommand);
CommandProcessingResult adjustLoanTransaction(Long loanId, Long transactionId, JsonCommand command);
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java
index b6540126a..7842dd7b4 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java
@@ -40,7 +40,8 @@ public enum PaymentAllocationTransactionType {
CHARGE_ADJUSTMENT(LoanTransactionType.CHARGE_ADJUSTMENT, "Charge adjustment"), //
WAIVE_INTEREST(LoanTransactionType.WAIVE_INTEREST, "Waive interest"), //
CHARGE_PAYMENT(LoanTransactionType.CHARGE_PAYMENT, "Charge payment"), //
- REFUND_FOR_ACTIVE_LOAN(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, "Refund for active loan");
+ REFUND_FOR_ACTIVE_LOAN(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, "Refund for active loan"), //
+ INTEREST_PAYMENT_WAIVER(LoanTransactionType.INTEREST_PAYMENT_WAIVER, "Interest payment waiver");
private final LoanTransactionType loanTransactionType;
private final String humanReadableName;
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 a30de5e9c..4c4110988 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
@@ -303,6 +303,8 @@ public final class LoanEnumerations {
LoanTransactionType.PAYOUT_REFUND.getCode(), "Payout Refund");
case GOODWILL_CREDIT -> new LoanTransactionEnumData(LoanTransactionType.GOODWILL_CREDIT.getValue().longValue(),
LoanTransactionType.GOODWILL_CREDIT.getCode(), "Goodwill Credit");
+ case INTEREST_PAYMENT_WAIVER -> new LoanTransactionEnumData(LoanTransactionType.INTEREST_PAYMENT_WAIVER.getValue().longValue(),
+ LoanTransactionType.INTEREST_PAYMENT_WAIVER.getCode(), "Interest Payment Waiver");
case CHARGE_REFUND -> new LoanTransactionEnumData(LoanTransactionType.CHARGE_REFUND.getValue().longValue(),
LoanTransactionType.CHARGE_REFUND.getCode(), "Charge Refund");
case CHARGEBACK -> new LoanTransactionEnumData(LoanTransactionType.CHARGEBACK.getValue().longValue(),
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index 9796c47fc..7513f0dbf 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -189,7 +189,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
case CREDIT_BALANCE_REFUND ->
handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
- WAIVE_INTEREST, RECOVERY_REPAYMENT ->
+ WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER ->
handleRepayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(), ctx.getOverpaymentHolder());
case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(),
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
index 51bab9942..82686cf78 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
@@ -110,6 +110,106 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
else if (loanTransactionDTO.getTransactionType().isChargeoff()) {
createJournalEntriesForChargeOff(loanDTO, loanTransactionDTO, office);
}
+ // Logic for Interest Payment Waiver
+ else if (loanTransactionDTO.getTransactionType().isInterestPaymentWaiver()) {
+ createJournalEntriesForInterestPaymentWaiver(loanDTO, loanTransactionDTO, office);
+ }
+ }
+ }
+
+ private void createJournalEntriesForInterestPaymentWaiver(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) {
+ final Long loanProductId = loanDTO.getLoanProductId();
+ final Long loanId = loanDTO.getLoanId();
+ final String currencyCode = loanDTO.getCurrencyCode();
+ final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff();
+
+ // transaction properties
+ final String transactionId = loanTransactionDTO.getTransactionId();
+ final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
+ final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
+ final BigDecimal interestAmount = loanTransactionDTO.getInterest();
+ final BigDecimal feesAmount = loanTransactionDTO.getFees();
+ final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
+ final BigDecimal overPayment = loanTransactionDTO.getOverPayment();
+ final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
+ final boolean isReversal = loanTransactionDTO.isReversed();
+
+ Map<GLAccount, BigDecimal> accountMapForCredit = new LinkedHashMap<>();
+
+ Map<Integer, BigDecimal> accountMapForDebit = new LinkedHashMap<>();
+
+ if (isMarkedAsChargeOff) {
+ // ChargeOFF
+ // principal payment
+ if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
+ populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId,
+ AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
+ AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
+ }
+ // interest payment
+ if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) {
+ populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId,
+ AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
+ AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
+ }
+ // handle fees payment
+ if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) {
+ populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId,
+ AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
+ AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
+ }
+
+ // handle penalty payment
+ if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) {
+ populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId,
+ AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
+ AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
+ }
+ // handle overpayment
+ if (overPayment != null && overPayment.compareTo(BigDecimal.ZERO) > 0) {
+ populateCreditDebitMaps(loanProductId, overPayment, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(),
+ AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
+ }
+ } else {
+ // principal payment
+ if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
+ populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
+ AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
+ }
+ // interest payment
+ if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) {
+ populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(),
+ AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
+ }
+ // handle fees payment
+ if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) {
+ populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(),
+ AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
+ }
+
+ // handle penalty payment
+ if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) {
+ populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId,
+ AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(),
+ accountMapForCredit, accountMapForDebit);
+ }
+ // handle overpayment
+ if (overPayment != null && overPayment.compareTo(BigDecimal.ZERO) > 0) {
+ populateCreditDebitMaps(loanProductId, overPayment, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(),
+ AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
+ }
+ }
+
+ // create credit entries
+ for (Map.Entry<GLAccount, BigDecimal> creditEntry : accountMapForCredit.entrySet()) {
+ this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate,
+ creditEntry.getValue(), isReversal, creditEntry.getKey());
+ }
+
+ // create debit entries
+ for (Map.Entry<Integer, BigDecimal> debitEntry : accountMapForDebit.entrySet()) {
+ this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId,
+ paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue(), isReversal);
}
}
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 98fd50342..943f5ba54 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
@@ -111,9 +111,9 @@ public class LoanTransactionsApiResource {
+ "loans/1/transactions/template?command=disburse" + "\n" + "loans/1/transactions/template?command=disburseToSavings" + "\n"
+ "loans/1/transactions/template?command=recoverypayment" + "\n" + "loans/1/transactions/template?command=prepayLoan" + "\n"
+ "loans/1/transactions/template?command=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n"
- + "loans/1/transactions/template?command=foreclosure" + "\n"
- + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)" + "\n"
- + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
+ + "loans/1/transactions/template?command=foreclosure" + "\n" + "loans/1/transactions/template?command=interestPaymentWaiver"
+ + "\n" + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)"
+ + "\n" + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
public String retrieveTransactionTemplate(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
@@ -140,9 +140,9 @@ public class LoanTransactionsApiResource {
+ "loans/1/transactions/template?command=disburse" + "\n" + "loans/1/transactions/template?command=disburseToSavings" + "\n"
+ "loans/1/transactions/template?command=recoverypayment" + "\n" + "loans/1/transactions/template?command=prepayLoan" + "\n"
+ "loans/1/transactions/template?command=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n"
- + "loans/1/transactions/template?command=foreclosure" + "\n"
- + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)" + "\n"
- + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
+ + "loans/1/transactions/template?command=foreclosure" + "\n" + "loans/1/transactions/template?command=interestPaymentWaiver"
+ + "\n" + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)"
+ + "\n" + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
public String retrieveTransactionTemplate(
@@ -455,6 +455,8 @@ public class LoanTransactionsApiResource {
commandRequest = builder.loanPayoutRefundTransaction(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, "goodwillCredit")) {
commandRequest = builder.loanGoodwillCreditTransaction(resolvedLoanId).build();
+ } else if (CommandParameterUtil.is(commandParam, "interestPaymentWaiver")) {
+ commandRequest = builder.loanInterestPaymentWaiverTransaction(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, "chargeRefund")) {
commandRequest = builder.refundLoanCharge(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, "waiveinterest")) {
@@ -521,6 +523,10 @@ public class LoanTransactionsApiResource {
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.GOODWILL_CREDIT,
resolvedLoanId, transactionDate);
+ } else if (CommandParameterUtil.is(commandParam, "interestPaymentWaiver")) {
+ LocalDate transactionDate = DateUtils.getBusinessLocalDate();
+ transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.INTEREST_PAYMENT_WAIVER,
+ resolvedLoanId, transactionDate);
} else if (CommandParameterUtil.is(commandParam, "waiveinterest")) {
transactionData = this.loanReadPlatformService.retrieveWaiveInterestDetails(resolvedLoanId);
} else if (CommandParameterUtil.is(commandParam, "writeoff")) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
index 5d6afe71e..f1219a47d 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
@@ -57,6 +57,8 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionDownPaymentPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionGoodwillCreditPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionGoodwillCreditPreBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMakeRepaymentPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMakeRepaymentPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMerchantIssuedRefundPostBusinessEvent;
@@ -292,6 +294,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
repaymentEvent = new LoanTransactionPayoutRefundPreBusinessEvent(loan);
} else if (repaymentTransactionType.isGoodwillCredit()) {
repaymentEvent = new LoanTransactionGoodwillCreditPreBusinessEvent(loan);
+ } else if (repaymentTransactionType.isInterestPaymentWaiver()) {
+ repaymentEvent = new LoanTransactionInterestPaymentWaiverPreBusinessEvent(loan);
} else if (repaymentTransactionType.isChargeRefund()) {
repaymentEvent = new LoanChargePaymentPreBusinessEvent(loan);
} else if (isRecoveryRepayment) {
@@ -313,6 +317,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
repaymentEvent = new LoanTransactionPayoutRefundPostBusinessEvent(transaction);
} else if (repaymentTransactionType.isGoodwillCredit()) {
repaymentEvent = new LoanTransactionGoodwillCreditPostBusinessEvent(transaction);
+ } else if (repaymentTransactionType.isInterestPaymentWaiver()) {
+ repaymentEvent = new LoanTransactionInterestPaymentWaiverPostBusinessEvent(transaction);
} else if (repaymentTransactionType.isChargeRefund()) {
repaymentEvent = new LoanChargePaymentPostBusinessEvent(transaction);
} else if (isRecoveryRepayment) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanInterestPaymentWaiverCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanInterestPaymentWaiverCommandHandler.java
new file mode 100644
index 000000000..df4a23042
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanInterestPaymentWaiverCommandHandler.java
@@ -0,0 +1,44 @@
+/**
+ * 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.DataIntegrityErrorHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "LOAN", action = "INTERESTPAYMENTWAIVER")
+public class LoanInterestPaymentWaiverCommandHandler implements NewCommandSourceHandler {
+
+ private final LoanWritePlatformService writePlatformService;
+ private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
+
+ @Transactional
+ @Override
+ public CommandProcessingResult processCommand(final JsonCommand command) {
+ return this.writePlatformService.makeInterestPaymentWaiver(command);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java
similarity index 75%
rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java
rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java
index e26c0f2df..4ff6935bc 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java
@@ -35,32 +35,54 @@ import java.util.Set;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.organisation.holiday.domain.Holiday;
+import org.apache.fineract.organisation.holiday.service.HolidayUtil;
+import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
+import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepository;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.organisation.monetary.exception.CurrencyNotFoundException;
+import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
+import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
import org.apache.fineract.portfolio.calendar.exception.NotValidRecurringDateException;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
+import org.apache.fineract.portfolio.client.domain.Client;
+import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
+import org.apache.fineract.portfolio.group.domain.Group;
+import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
+import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
+import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
+import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanRepaymentScheduleNotFoundException;
+import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.springframework.stereotype.Component;
@Component
@AllArgsConstructor
-public final class LoanEventApiJsonValidator {
+public final class LoanTransactionValidator {
private final FromJsonHelper fromApiJsonHelper;
private final LoanApplicationValidator fromApiJsonDeserializer;
private final LoanRepository loanRepository;
+ private final ApplicationCurrencyRepository applicationCurrencyRepository;
+ private final LoanUtilService loanUtilService;
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
@@ -562,4 +584,154 @@ public final class LoanEventApiJsonValidator {
validatePaymentDetails(baseDataValidator, element);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
+
+ public void validateLoanClientIsActive(final Loan loan) {
+ final Client client = loan.client();
+ if (client != null && client.isNotActive()) {
+ throw new ClientNotActiveException(client.getId());
+ }
+ }
+
+ public void validateLoanGroupIsActive(final Loan loan) {
+ final Group group = loan.group();
+ if (group != null && group.isNotActive()) {
+ throw new GroupNotActiveException(group.getId());
+ }
+ }
+
+ public void validateLoanStatusIsActiveOrFullyPaidOrOverpaid(final Loan loan) {
+ if (!(loan.isOpen() || loan.isClosedObligationsMet() || loan.isOverPaid())) {
+ final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+ final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid";
+ final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.must.be.active.fully.paid.or.overpaid",
+ defaultUserMessage);
+ dataValidationErrors.add(error);
+ throw new PlatformApiDataValidationException(dataValidationErrors);
+ }
+ }
+
+ public void validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(Loan loan, LocalDate transactionDate,
+ String reversedOrCreated) {
+ for (LoanTransaction txn : loan.getLoanTransactions()) {
+ if (txn.isChargeRefund() && DateUtils.isBefore(transactionDate, txn.getTransactionDate())) {
+ final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists";
+ final String details = "Loan Transaction: " + loan.getId() + " Can't be " + reversedOrCreated
+ + " because a Later Charge Refund Exists.";
+ throw new LoanChargeRefundException(errorMessage, details);
+ }
+ }
+ }
+
+ public void validateLoanDisbursementIsBeforeTransactionDate(final Loan loan, final LocalDate transactionDate) {
+ if (DateUtils.isBefore(transactionDate, loan.getDisbursementDate())) {
+ final String errorMessage = "The transaction date cannot be before the loan disbursement date: "
+ + loan.getDisbursementDate().toString();
+ throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage,
+ transactionDate, loan.getDisbursementDate());
+ }
+ }
+
+ public void validateTransactionShouldNotBeInTheFuture(final LocalDate transactionDate) {
+ if (DateUtils.isDateInTheFuture(transactionDate)) {
+ final String errorMessage = "The transaction date cannot be in the future.";
+ throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, transactionDate);
+ }
+ }
+
+ public void validateLoanHasCurrency(final Loan loan) {
+ MonetaryCurrency currency = loan.getCurrency();
+ final ApplicationCurrency defaultApplicationCurrency = this.applicationCurrencyRepository.findOneByCode(currency.getCode());
+ if (defaultApplicationCurrency == null) {
+ throw new CurrencyNotFoundException(currency.getCode());
+ }
+ }
+
+ public void validateClientOfficeJoiningDateIsBeforeTransactionDate(Loan loan, LocalDate transactionDate) {
+ if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) {
+ final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate();
+ if (DateUtils.isBefore(transactionDate, clientOfficeJoiningDate)) {
+ String errorMessage = "The date on which a repayment or waiver is made cannot be earlier than client's transfer date to this office";
+ String action = "repayment.or.waiver";
+ String postfix = "cannot.be.made.before.client.transfer.date";
+ throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, clientOfficeJoiningDate);
+ }
+ }
+ }
+
+ public void validateActivityNotBeforeLastTransactionDate(final Loan loan, final LocalDate activityDate) {
+ if (!(loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || loan.loanProduct().isHoldGuaranteeFunds())) {
+ return;
+ }
+ LocalDate lastTransactionDate = loan.getLastUserTransactionDate();
+ if (DateUtils.isAfter(lastTransactionDate, activityDate)) {
+ String errorMessage = "The date on which a repayment or waiver is made cannot be earlier than last transaction date";
+ String action = "repayment.or.waiver";
+ String postfix = "cannot.be.made.before.last.transaction.date";
+ throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, lastTransactionDate);
+ }
+ }
+
+ public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays,
+ final boolean allowTransactionsOnNonWorkingDay) {
+ if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) {
+ final String errorMessage = "Repayment date cannot be on a non working day";
+ throw new LoanApplicationDateException("repayment.date.on.non.working.day", errorMessage, repaymentDate);
+ }
+ }
+
+ public void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean allowTransactionsOnHoliday,
+ final List<Holiday> holidays) {
+ if (!allowTransactionsOnHoliday && HolidayUtil.isHoliday(repaymentDate, holidays)) {
+ final String errorMessage = "Repayment date cannot be on a holiday";
+ throw new LoanApplicationDateException("repayment.date.on.holiday", errorMessage, repaymentDate);
+ }
+ }
+
+ public void validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(Loan loan) {
+ if (loan.getLoanProduct().isMultiDisburseLoan()) {
+ BigDecimal totalDisbursed = loan.getDisbursedAmount();
+ BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
+ BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
+ if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0) {
+ final String errorMessage = "The transaction amount cannot exceed threshold.";
+ throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
+ }
+ }
+ }
+
+ public void validateLoanTransactionInterestPaymentWaiver(JsonCommand command) {
+ final Long loanId = command.getLoanId();
+ Loan loan = this.loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
+ final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
+ validateNewRepaymentTransaction(command.json());
+ validateTransactionShouldNotBeInTheFuture(transactionDate);
+ validateLoanClientIsActive(loan);
+ validateLoanHasCurrency(loan);
+ validateLoanGroupIsActive(loan);
+ validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan);
+ validateLoanDisbursementIsBeforeTransactionDate(loan, transactionDate);
+ validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(loan, transactionDate, "created");
+
+ validateClientOfficeJoiningDateIsBeforeTransactionDate(loan, transactionDate);
+ validateActivityNotBeforeLastTransactionDate(loan, transactionDate);
+ HolidayDetailDTO holidayDetailDTO = loanUtilService.constructHolidayDTO(loan.getOfficeId(), loan.getDisbursementDate());
+ validateRepaymentDateIsOnHoliday(transactionDate, holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
+ validateRepaymentDateIsOnNonWorkingDay(transactionDate, holidayDetailDTO.getWorkingDays(),
+ holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
+ validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan);
+ }
+
+ public void validateLoanTransactionInterestPaymentWaiverAfterRecalculation(Loan loan) {
+ // Payment allocation calculates the new total principal repaid, and it should be validated after recalculation
+ if (loan.getLoanProduct().isMultiDisburseLoan()) {
+ BigDecimal totalDisbursed = loan.getDisbursedAmount();
+ BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
+ BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
+ if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0
+ && loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) {
+ final String errorMessage = "The transaction amount cannot exceed threshold.";
+ throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
+ }
+ }
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionAssembler.java
new file mode 100644
index 000000000..6674a69d1
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionAssembler.java
@@ -0,0 +1,68 @@
+/**
+ * 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.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
+import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
+import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
+
+@RequiredArgsConstructor
+public class LoanTransactionAssembler {
+
+ private final ExternalIdFactory externalIdFactory;
+ private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
+
+ LoanTransaction assembleTransactionAndCalculateChanges(Loan loan, JsonCommand command, Map<String, Object> changes) {
+ final String noteText = command.stringValueOfParameterNamed("note");
+ final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
+ changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
+ changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
+ changes.put("locale", command.locale());
+ changes.put("dateFormat", command.dateFormat());
+ changes.put("paymentTypeId", command.longValueOfParameterNamed("paymentTypeId"));
+
+ if (StringUtils.isNotBlank(noteText)) {
+ changes.put("note", noteText);
+ }
+ if (!txnExternalId.isEmpty()) {
+ changes.put(LoanApiConstants.externalIdParameterName, txnExternalId);
+ }
+
+ final PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
+ final LoanTransactionType repaymentTransactionType = LoanTransactionType.INTEREST_PAYMENT_WAIVER;
+ final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
+
+ final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
+ final Money repaymentAmount = Money.of(loan.getCurrency(), transactionAmount);
+ return LoanTransaction.repaymentType(repaymentTransactionType, loan.getOffice(), repaymentAmount, paymentDetail, transactionDate,
+ txnExternalId, null);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java
index 614f8f24a..824178d49 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java
@@ -175,6 +175,17 @@ public class LoanUtilService {
return holidayDetailDTO;
}
+ public HolidayDetailDTO constructHolidayDTO(final Long officeId, LocalDate localDate) {
+ final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
+ final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, localDate,
+ HolidayStatusType.ACTIVE.getValue());
+ final WorkingDays workingDays = this.workingDaysRepository.findOne();
+ final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled();
+ final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
+
+ return new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay);
+ }
+
private FloatingRateDTO constructFloatingRateDTO(final Loan loan) {
FloatingRateDTO floatingRateDTO = null;
if (loan.loanProduct().isLinkedToFloatingInterestRate()) {
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 2743623a4..b5e91f434 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
@@ -84,6 +84,9 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoChargeOffBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWaiveInterestBusinessEvent;
@@ -109,7 +112,9 @@ import org.apache.fineract.portfolio.account.domain.AccountAssociationsRepositor
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
+import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
+import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
@@ -162,7 +167,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
+import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
+import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException;
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
@@ -180,7 +188,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
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;
-import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator;
+import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
@@ -188,6 +196,7 @@ 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.apache.fineract.portfolio.repaymentwithpostdatedchecks.data.PostDatedChecksStatus;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.service.RepaymentWithPostDatedChecksAssembler;
@@ -203,7 +212,7 @@ import org.springframework.transaction.annotation.Transactional;
public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService {
private final PlatformSecurityContext context;
- private final LoanEventApiJsonValidator loanEventApiJsonValidator;
+ private final LoanTransactionValidator loanTransactionValidator;
private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanAccountDomainService loanAccountDomainService;
@@ -247,6 +256,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final ErrorHandler errorHandler;
private final LoanDownPaymentHandlerService loanDownPaymentHandlerService;
+ private final AccountTransferRepository accountTransferRepository;
+ private final LoanTransactionAssembler loanTransactionAssembler;
@Transactional
@Override
@@ -283,11 +294,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final AppUser currentUser = getAppUserIfPresent();
- this.loanEventApiJsonValidator.validateDisbursement(command.json(), isAccountTransfer);
+ this.loanTransactionValidator.validateDisbursement(command.json(), isAccountTransfer);
if (command.parameterExists("postDatedChecks")) {
// validate with post dated checks for the disbursement
- this.loanEventApiJsonValidator.validateDisbursementWithPostDatedChecks(command.json(), loanId);
+ this.loanTransactionValidator.validateDisbursementWithPostDatedChecks(command.json(), loanId);
}
Loan loan = this.loanAssembler.assembleFrom(loanId);
@@ -364,7 +375,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
if (loan.isSyncDisbursementWithMeeting()) {
- this.loanEventApiJsonValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance,
+ this.loanTransactionValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance,
scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), scheduleGeneratorDTO.getNumberOfdays());
}
@@ -956,13 +967,307 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
chargeRefundChargeType);
}
+ private void handleLoanOverpayment(Loan loan, LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
+ loan.setOverpaidOnDate(transactionDate);
+ loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, loan);
+ loan.setClosedOnDate(null);
+ loan.setActualMaturityDate(null);
+ }
+
+ private void handleLoanRepaymentInFull(final Loan loan, final LocalDate transactionDate,
+ final LoanLifecycleStateMachine loanLifecycleStateMachine) {
+
+ boolean isAllChargesPaid = true;
+ for (final LoanCharge loanCharge : loan.getCharges()) {
+ if (loanCharge.isActive() && loanCharge.amount().compareTo(BigDecimal.ZERO) > 0
+ && !(loanCharge.isPaid() || loanCharge.isWaived())) {
+ isAllChargesPaid = false;
+ break;
+ }
+ }
+ if (isAllChargesPaid) {
+ loan.setClosedOnDate(transactionDate);
+ loan.setActualMaturityDate(transactionDate);
+ loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, loan);
+ } else if (LoanStatus.fromInt(loan.getLoanStatus()).isOverpaid()) {
+ if (loan.getTotalOverpaid() == null || BigDecimal.ZERO.compareTo(loan.getTotalOverpaid()) == 0) {
+ loan.setOverpaidOnDate(null);
+ }
+ loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
+ }
+ loan.processIncomeAccrualTransactionOnLoanClosure();
+ }
+
+ private boolean doPostLoanTransactionChecks(final Loan loan, final LocalDate transactionDate,
+ final LoanLifecycleStateMachine loanLifecycleStateMachine) {
+ boolean statusChanged = false;
+ boolean isOverpaid = loan.getTotalOverpaid() != null && loan.getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0;
+ if (isOverpaid) {
+ // FIXME - kw - update account balance to negative amount.
+ handleLoanOverpayment(loan, transactionDate, loanLifecycleStateMachine);
+ statusChanged = true;
+ } else if (loan.getSummary().isRepaidInFull(loan.loanCurrency())) {
+ handleLoanRepaymentInFull(loan, transactionDate, loanLifecycleStateMachine);
+ statusChanged = true;
+ } else {
+ loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
+ }
+ if (loan.getTotalOverpaid() == null || BigDecimal.ZERO.compareTo(loan.getTotalOverpaid()) == 0) {
+ loan.setOverpaidOnDate(null);
+ }
+ return statusChanged;
+ }
+
+ private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
+ final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
+ if (transferTransaction != null) {
+ transferTransaction.updateToLoanTransaction(newLoanTransaction);
+ this.accountTransferRepository.save(transferTransaction);
+ }
+ }
+
+ // TODO check
+ public boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction,
+ final List<LoanTransaction> loanTransactions) {
+ boolean isChronologicallyLatestRepaymentOrWaiver = true;
+
+ final LocalDate currentTransactionDate = loanTransaction.getTransactionDate();
+ for (final LoanTransaction previousTransaction : loanTransactions) {
+ if (!previousTransaction.isDisbursement() && previousTransaction.isNotReversed()
+ && (DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate())
+ || (DateUtils.isEqual(currentTransactionDate, previousTransaction.getTransactionDate())
+ && ((loanTransaction.getId() == null && previousTransaction.getId() == null)
+ || (loanTransaction.getId() != null && (previousTransaction.getId() == null
+ || loanTransaction.getId().compareTo(previousTransaction.getId()) < 0)))))) {
+ isChronologicallyLatestRepaymentOrWaiver = false;
+ break;
+ }
+ }
+ return isChronologicallyLatestRepaymentOrWaiver;
+ }
+
+ private ChangedTransactionDetail recalculateLoanWithInterestPaymentWaiverTxn(Loan loan,
+ LoanTransaction newInterestPaymentWaiverTransaction) {
+ LocalDate recalculateFrom = null;
+ if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
+ recalculateFrom = newInterestPaymentWaiverTransaction.getTransactionDate();
+ }
+ final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, null);
+
+ ChangedTransactionDetail changedTransactionDetail = null;
+ newInterestPaymentWaiverTransaction.updateLoan(loan);
+
+ final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(newInterestPaymentWaiverTransaction,
+ loan.getLoanTransactions());
+
+ if (newInterestPaymentWaiverTransaction.isNotZero(loan.getLoanRepaymentScheduleDetail().getCurrency())) {
+ loan.addLoanTransaction(newInterestPaymentWaiverTransaction);
+ }
+
+ final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessorFactory()
+ .determineProcessor(loan.getTransactionProcessingStrategyCode());
+
+ final LoanRepaymentScheduleInstallment currentInstallment = loan
+ .fetchLoanRepaymentScheduleInstallment(newInterestPaymentWaiverTransaction.getTransactionDate());
+
+ boolean reprocess = true;
+
+ if (!loan.isForeclosure() && isTransactionChronologicallyLatest
+ && DateUtils.isEqualBusinessDate(newInterestPaymentWaiverTransaction.getTransactionDate()) && currentInstallment != null
+ && currentInstallment.getTotalOutstanding(loan.getCurrency())
+ .isEqualTo(newInterestPaymentWaiverTransaction.getAmount(loan.getCurrency()))) {
+ reprocess = false;
+ }
+
+ if (isTransactionChronologicallyLatest && (!reprocess || !loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled())
+ && !loan.isForeclosure()) {
+ loanRepaymentScheduleTransactionProcessor.processLatestTransaction(newInterestPaymentWaiverTransaction,
+ new LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(),
+ loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
+ new MoneyHolder(loan.getTotalOverpaidAsMoney())));
+ reprocess = false;
+ if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
+ if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
+ reprocess = true;
+ } else {
+ final LoanRepaymentScheduleInstallment nextInstallment = loan
+ .fetchRepaymentScheduleInstallment(currentInstallment.getInstallmentNumber() + 1);
+ if (nextInstallment != null && nextInstallment.getTotalPaidInAdvance(loan.getCurrency()).isGreaterThanZero()) {
+ reprocess = true;
+ }
+ }
+ }
+ }
+ if (reprocess) {
+ changedTransactionDetail = reprocessChangedLoanTransactions(loan, loanRepaymentScheduleTransactionProcessor,
+ scheduleGeneratorDTO);
+ }
+
+ loan.updateLoanSummaryDerivedFields();
+
+ doPostLoanTransactionChecks(loan, newInterestPaymentWaiverTransaction.getTransactionDate(), defaultLoanLifecycleStateMachine);
+
+ return changedTransactionDetail;
+ }
+
+ private ChangedTransactionDetail reprocessChangedLoanTransactions(Loan loan,
+ LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
+ ScheduleGeneratorDTO scheduleGeneratorDTO) {
+ if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
+ loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
+ }
+ final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
+ ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
+ loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(),
+ loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
+ for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
+ mapEntry.getValue().updateLoan(loan);
+ }
+ /***
+ * Commented since throwing exception if external id present for one of the transactions. for this need to save
+ * the reversed transactions first and then new transactions.
+ */
+ loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values());
+ return changedTransactionDetail;
+ }
+
+ @Transactional
+ @Override
+ public CommandProcessingResult makeInterestPaymentWaiver(final JsonCommand command) {
+
+ loanTransactionValidator.validateLoanTransactionInterestPaymentWaiver(command);
+
+ final Long loanId = command.getLoanId();
+ Loan loan = this.loanAssembler.assembleFrom(loanId);
+
+ businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionInterestPaymentWaiverPreBusinessEvent(loan));
+
+ // save already existing transaction ids
+ final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
+ final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
+
+ final String noteText = command.stringValueOfParameterNamed("note");
+ final Map<String, Object> changes = new LinkedHashMap<>();
+
+ LoanTransaction newInterestPaymentWaiverTransaction = loanTransactionAssembler.assembleTransactionAndCalculateChanges(loan, command,
+ changes);
+
+ ChangedTransactionDetail changedTransactionDetail = recalculateLoanWithInterestPaymentWaiverTxn(loan,
+ newInterestPaymentWaiverTransaction);
+
+ loanTransactionValidator.validateLoanTransactionInterestPaymentWaiverAfterRecalculation(loan);
+
+ loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newInterestPaymentWaiverTransaction);
+
+ saveChangedTransactionDetail(changedTransactionDetail);
+
+ loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
+
+ saveNote(noteText, loan, newInterestPaymentWaiverTransaction);
+
+ postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
+ loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
+ loanAccountDomainService.recalculateAccruals(loan);
+ loanAccountDomainService.setLoanDelinquencyTag(loan, newInterestPaymentWaiverTransaction.getTransactionDate());
+
+ // disable all active standing orders linked to this loan if status changes to closed
+ loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan);
+
+ updateAndSavePostDatedChecksForIndividualAccount(loan, newInterestPaymentWaiverTransaction);
+ updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, newInterestPaymentWaiverTransaction);
+
+ // Finished Notification
+
+ LoanTransactionBusinessEvent transactionRepaymentEvent = new LoanTransactionInterestPaymentWaiverPostBusinessEvent(
+ newInterestPaymentWaiverTransaction);
+ businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
+ businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
+
+ return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
+ .withLoanId(loan.getId()) //
+ .withEntityId(newInterestPaymentWaiverTransaction.getId()) //
+ .withEntityExternalId(newInterestPaymentWaiverTransaction.getExternalId()) //
+ .withOfficeId(loan.getOfficeId()) //
+ .withClientId(loan.getClientId()) //
+ .withGroupId(loan.getGroupId()) //
+ .with(changes) //
+ .build();
+ }
+
+ private void saveNote(String noteText, Loan loan, LoanTransaction loanTransaction) {
+ if (StringUtils.isNotBlank(noteText)) {
+ final Note note = Note.loanTransactionNote(loan, loanTransaction, noteText);
+ this.noteRepository.save(note);
+ }
+ }
+
+ private void saveChangedTransactionDetail(ChangedTransactionDetail changedTransactionDetail) {
+ if (changedTransactionDetail != null) {
+ for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
+ loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
+ updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
+ }
+ // Trigger transaction replayed event
+ replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
+ }
+ }
+
+ private void updateAndSavePostDatedChecksForIndividualAccount(Loan loan, LoanTransaction loanTransaction) {
+ if (loan.getLoanType().isIndividualAccount()) {
+ // Mark Post Dated Check as paid.
+ final Set<LoanTransactionToRepaymentScheduleMapping> loanTransactionToRepaymentScheduleMappings = loanTransaction
+ .getLoanTransactionToRepaymentScheduleMappings();
+
+ if (loanTransactionToRepaymentScheduleMappings != null) {
+ for (LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping : loanTransactionToRepaymentScheduleMappings) {
+ LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanTransactionToRepaymentScheduleMapping
+ .getLoanRepaymentScheduleInstallment();
+ if (loanRepaymentScheduleInstallment != null) {
+ final boolean isPaid = loanRepaymentScheduleInstallment.isNotFullyPaidOff();
+ PostDatedChecks postDatedChecks = this.postDatedChecksRepository
+ .getPendingPostDatedCheck(loanRepaymentScheduleInstallment);
+
+ if (postDatedChecks != null) {
+ if (!isPaid) {
+ postDatedChecks.setStatus(PostDatedChecksStatus.POST_DATED_CHECKS_PAID);
+ } else {
+ postDatedChecks.setStatus(PostDatedChecksStatus.POST_DATED_CHECKS_PENDING);
+ }
+ this.postDatedChecksRepository.saveAndFlush(postDatedChecks);
+ } else {
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void updateAndSaveLoanCollateralTransactionsForIndividualAccounts(Loan loan, LoanTransaction loanTransaction) {
+ if (loan.getLoanType().isIndividualAccount()) {
+ Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements();
+ for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) {
+ loanCollateralManagement.setLoanTransactionData(loanTransaction);
+ ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement();
+
+ if (loan.getStatus().isClosed()) {
+ loanCollateralManagement.setIsReleased(true);
+ BigDecimal quantity = loanCollateralManagement.getQuantity();
+ clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity));
+ loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement);
+ }
+ }
+ this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements);
+ }
+ }
+
@Transactional
@Override
public CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final LoanTransactionType repaymentTransactionType,
final Long loanId, final JsonCommand command, final boolean isRecoveryRepayment, final String chargeRefundChargeType) {
this.loanUtilService.validateRepaymentTransactionType(repaymentTransactionType);
- this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json());
+ this.loanTransactionValidator.validateNewRepaymentTransaction(command.json());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
@@ -1083,7 +1388,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) {
- this.loanEventApiJsonValidator.validateTransaction(command.json());
+ this.loanTransactionValidator.validateTransaction(command.json());
LoanTransaction transactionToAdjust = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId())
.orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId()));
@@ -1255,7 +1560,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional
@Override
public CommandProcessingResult chargebackLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) {
- this.loanEventApiJsonValidator.validateChargebackTransaction(command.json());
+ this.loanTransactionValidator.validateChargebackTransaction(command.json());
LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId())
.orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId()));
@@ -1369,7 +1674,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult waiveInterestOnLoan(final Long loanId, final JsonCommand command) {
- this.loanEventApiJsonValidator.validateTransaction(command.json());
+ this.loanTransactionValidator.validateTransaction(command.json());
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
@@ -1456,7 +1761,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
public CommandProcessingResult writeOff(final Long loanId, final JsonCommand command) {
final AppUser currentUser = getAppUserIfPresent();
- this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
+ this.loanTransactionValidator.validateTransactionWithNoAmount(command.json());
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
@@ -1534,7 +1839,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult closeLoan(final Long loanId, final JsonCommand command) {
- this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
+ this.loanTransactionValidator.validateTransactionWithNoAmount(command.json());
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
@@ -1643,7 +1948,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult closeAsRescheduled(final Long loanId, final JsonCommand command) {
- this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
+ this.loanTransactionValidator.validateTransactionWithNoAmount(command.json());
final Loan loan = this.loanAssembler.assembleFrom(loanId);
checkClientOrGroupActive(loan);
@@ -1839,7 +2144,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult loanReassignment(final Long loanId, final JsonCommand command) {
- this.loanEventApiJsonValidator.validateUpdateOfLoanOfficer(command.json());
+ this.loanTransactionValidator.validateUpdateOfLoanOfficer(command.json());
final Long fromLoanOfficerId = command.longValueOfParameterNamed("fromLoanOfficerId");
final Long toLoanOfficerId = command.longValueOfParameterNamed("toLoanOfficerId");
@@ -1875,7 +2180,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult bulkLoanReassignment(final JsonCommand command) {
- this.loanEventApiJsonValidator.validateForBulkLoanReassignment(command.json());
+ this.loanTransactionValidator.validateForBulkLoanReassignment(command.json());
final Long fromLoanOfficerId = command.longValueOfParameterNamed("fromLoanOfficerId");
final Long toLoanOfficerId = command.longValueOfParameterNamed("toLoanOfficerId");
@@ -2412,7 +2717,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
"Update Loan: " + loanId + " disbursement details is not allowed. Loan Account is Charged-off", loanId);
}
LoanDisbursementDetails loanDisbursementDetails = loan.fetchLoanDisbursementsById(disbursementId);
- this.loanEventApiJsonValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails);
+ this.loanTransactionValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails);
CommandProcessingResult result = processLoanDisbursementDetail(loan, loanId, command, loanDisbursementDetails);
businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateDisbursementDataBusinessEvent(loan));
@@ -2506,7 +2811,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
public CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command) {
- this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
+ this.loanTransactionValidator.validateNewRefundTransaction(command.json());
Loan loan = this.loanAssembler.assembleFrom(loanId);
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
@@ -2551,7 +2856,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
@Transactional
public CommandProcessingResult markLoanAsFraud(Long loanId, JsonCommand command) {
- this.loanEventApiJsonValidator.validateMarkAsFraudLoan(command.json());
+ this.loanTransactionValidator.validateMarkAsFraudLoan(command.json());
Loan loan = this.loanAssembler.assembleFrom(loanId);
final Map<String, Object> changes = new LinkedHashMap<>();
@@ -2578,7 +2883,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional
public CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command) {
- this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
+ this.loanTransactionValidator.validateNewRefundTransaction(command.json());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
@@ -2696,7 +3001,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Loan loan = this.loanAssembler.assembleFrom(loanId);
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.transactionDateParamName, element);
final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
- this.loanEventApiJsonValidator.validateLoanForeclosure(command.json());
+ this.loanTransactionValidator.validateLoanForeclosure(command.json());
final Map<String, Object> changes = new LinkedHashMap<>();
// Got changed to match with the rest of the APIs
changes.put("dateFormat", command.dateFormat());
@@ -2732,7 +3037,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional
public CommandProcessingResult chargeOff(JsonCommand command) {
- loanEventApiJsonValidator.validateChargeOffTransaction(command.json());
+ loanTransactionValidator.validateChargeOffTransaction(command.json());
final Map<String, Object> changes = new LinkedHashMap<>();
changes.put(LoanApiConstants.transactionDateParamName,
@@ -2807,7 +3112,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override
@Transactional
public CommandProcessingResult undoChargeOff(JsonCommand command) {
- this.loanEventApiJsonValidator.validateUndoChargeOff(command.json());
+ this.loanTransactionValidator.validateUndoChargeOff(command.json());
final Long loanId = command.getLoanId();
final Loan loan = this.loanAssembler.assembleFrom(loanId);
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
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 99b3f7fa6..89ba8a678 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
@@ -43,6 +43,7 @@ import org.apache.fineract.organisation.teller.data.CashierTransactionDataValida
import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
import org.apache.fineract.portfolio.account.domain.AccountAssociationsRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
+import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountNumberGenerator;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
@@ -87,7 +88,7 @@ import org.apache.fineract.portfolio.loanaccount.mapper.LoanTransactionRelationM
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
-import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator;
+import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformServiceImpl;
@@ -121,6 +122,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformServiceImpl;
+import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl;
@@ -253,6 +255,14 @@ public class LoanAccountConfiguration {
calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper);
}
+ @Bean
+ @ConditionalOnMissingBean(LoanTransactionAssembler.class)
+ public LoanTransactionAssembler loanTransactionAssembler(ExternalIdFactory externalIdFactory,
+ PaymentDetailWritePlatformService paymentDetailWritePlatformService) {
+
+ return new LoanTransactionAssembler(externalIdFactory, paymentDetailWritePlatformService);
+ }
+
@Bean
@ConditionalOnMissingBean(LoanCalculateRepaymentPastDueService.class)
public LoanCalculateRepaymentPastDueService loanCalculateRepaymentPastDueService() {
@@ -362,7 +372,7 @@ public class LoanAccountConfiguration {
@Bean
@ConditionalOnMissingBean(LoanWritePlatformService.class)
public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext context,
- LoanEventApiJsonValidator loanEventApiJsonValidator,
+ LoanTransactionValidator loanTransactionValidator,
LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer, LoanRepositoryWrapper loanRepositoryWrapper,
LoanAccountDomainService loanAccountDomainService, NoteRepository noteRepository,
LoanTransactionRepository loanTransactionRepository, LoanTransactionRelationRepository loanTransactionRelationRepository,
@@ -386,8 +396,9 @@ public class LoanAccountConfiguration {
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccountLockService loanAccountLockService,
ExternalIdFactory externalIdFactory, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService,
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ErrorHandler errorHandler,
- LoanDownPaymentHandlerService loanDownPaymentHandlerService) {
- return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanEventApiJsonValidator, loanUpdateCommandFromApiJsonDeserializer,
+ LoanDownPaymentHandlerService loanDownPaymentHandlerService, AccountTransferRepository accountTransferRepository,
+ LoanTransactionAssembler loanTransactionAssembler) {
+ return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
paymentDetailWritePlatformService, holidayRepository, configurationDomainService, workingDaysRepository,
@@ -398,7 +409,8 @@ public class LoanAccountConfiguration {
transactionProcessingStrategy, codeValueRepository, cashierTransactionDataValidator, glimRepository, loanRepository,
repaymentWithPostDatedChecksAssembler, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository,
defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService,
- loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService);
+ loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository,
+ loanTransactionAssembler);
}
@Bean
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 43aa91ab8..be97efde9 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
@@ -160,4 +160,5 @@
<include file="parts/0138_add_external_event_for_loan_reaging_reamortization_2.xml" relativeToChangelogFile="true" />
<include file="parts/0139_add_disburse_without_auto_payment_command.xml" relativeToChangelogFile="true" />
<include file="parts/0140_trial_balance_with_asset_transfer_update.xml" relativeToChangelogFile="true" />
+ <include file="parts/0141_add_interest_payment_waiver_transaction_type.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0141_add_interest_payment_waiver_transaction_type.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0141_add_interest_payment_waiver_transaction_type.xml
new file mode 100644
index 000000000..c1bd5374f
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0141_add_interest_payment_waiver_transaction_type.xml
@@ -0,0 +1,49 @@
+<?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.3.xsd">
+ <changeSet author="fineract" id="1">
+ <insert tableName="m_permission">
+ <column name="grouping" value="transaction_loan" />
+ <column name="code" value="INTERESTPAYMENTWAIVER_LOAN" />
+ <column name="entity_name" value="LOAN" />
+ <column name="action_name" value="INTERESTPAYMENTWAIVER" />
+ <column name="can_maker_checker" valueBoolean="false" />
+ </insert>
+ <insert tableName="m_external_event_configuration">
+ <column name="type" value="LoanTransactionInterestPaymentWaiverPostBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ <insert tableName="m_external_event_configuration">
+ <column name="type" value="LoanTransactionInterestPaymentWaiverPreBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ <insert tableName="r_enum_value">
+ <column name="enum_name" value="transaction_type_enum"/>
+ <column name="enum_id" valueNumeric="31"/>
+ <column name="enum_message_property" value="Interest Payment Waiver"/>
+ <column name="enum_value" value="Interest Payment Waiver"/>
+ <column name="enum_type" 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 2e25f354f..c2f12a18e 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
@@ -101,7 +101,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent",
"LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent",
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
- "LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent");
+ "LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
+ "LoanTransactionInterestPaymentWaiverPostBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -184,7 +185,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent",
"LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent",
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
- "LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent");
+ "LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
+ "LoanTransactionInterestPaymentWaiverPostBusinessEvent");
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 800474ada..25aca2974 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
@@ -138,6 +138,7 @@ public abstract class BaseLoanIntegrationTest {
protected final Account chargeOffFraudExpenseAccount = accountHelper.createExpenseAccount("chargeOffFraud");
protected final Account writtenOffAccount = accountHelper.createExpenseAccount();
protected final Account goodwillExpenseAccount = accountHelper.createExpenseAccount();
+ protected final Account goodwillIncomeAccount = accountHelper.createIncomeAccount();
protected final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
protected ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
@@ -710,6 +711,14 @@ public abstract class BaseLoanIntegrationTest {
return response.getResourceId();
}
+ protected Long addInterestPaymentWaiverForLoan(Long loanId, Double amount, String date) {
+ String firstRepaymentUUID = UUID.randomUUID().toString();
+ PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeInterestPaymentWaiver(loanId,
+ new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(date).locale("en")
+ .transactionAmount(amount).externalId(firstRepaymentUUID));
+ return response.getResourceId();
+ }
+
protected Long chargeOffLoan(Long loanId, String date) {
String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5);
Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java
index 48c703651..fda6d2f07 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java
@@ -235,6 +235,62 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests {
loanProductError.get(0).get("defaultUserMessage"));
}
+ @Test
+ public void testCreateAndReadLoanProductWithAdvancedPaymentAndInterestPaymentWaiverTransaction() {
+ // given
+ AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
+ AdvancedPaymentData interestPaymentWaiverAllocation = createInterestPaymentWaiverAllocation();
+
+ // when
+ Integer loanProductId = LOAN_TRANSACTION_HELPER
+ .getLoanProductId(createLoanJSON(defaultAllocation, interestPaymentWaiverAllocation));
+ Assertions.assertNotNull(loanProductId);
+ GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
+
+ // then
+ Assertions.assertNotNull(loanProduct.getPaymentAllocation());
+ Assertions.assertEquals(2, loanProduct.getPaymentAllocation().size());
+ Optional<AdvancedPaymentData> first = loanProduct.getPaymentAllocation().stream()
+ .filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst();
+ Assertions.assertTrue(first.isPresent());
+ Assertions.assertEquals(defaultAllocation, first.get());
+
+ Optional<AdvancedPaymentData> second = loanProduct.getPaymentAllocation().stream()
+ .filter(advancedPaymentData -> "INTEREST_PAYMENT_WAIVER".equals(advancedPaymentData.getTransactionType())).findFirst();
+ Assertions.assertTrue(second.isPresent());
+ Assertions.assertEquals(interestPaymentWaiverAllocation, second.get());
+ }
+
+ @Test
+ public void testUpdateLoanProductInterestPaymentWaiverAllocationIsAdded() {
+ // given a loan with one allocation
+ AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
+ AdvancedPaymentData interestPaymentWaiverAllocation = createInterestPaymentWaiverAllocation();
+ Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation));
+ Assertions.assertNotNull(loanProductId);
+ GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
+ Assertions.assertNotNull(loanProduct.getPaymentAllocation());
+ Assertions.assertEquals(1, loanProduct.getPaymentAllocation().size());
+
+ // when a new allocation is added
+ LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(),
+ updateLoanProductRequest(defaultAllocation, interestPaymentWaiverAllocation));
+
+ // then it shall be added.
+ loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
+ Assertions.assertNotNull(loanProduct.getPaymentAllocation());
+ Assertions.assertEquals(2, loanProduct.getPaymentAllocation().size());
+ Optional<AdvancedPaymentData> first = loanProduct.getPaymentAllocation().stream()
+ .filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst();
+ Assertions.assertTrue(first.isPresent());
+ Assertions.assertEquals(defaultAllocation, first.get());
+
+ Optional<AdvancedPaymentData> second = loanProduct.getPaymentAllocation().stream()
+ .filter(advancedPaymentData -> "INTEREST_PAYMENT_WAIVER".equals(advancedPaymentData.getTransactionType())).findFirst();
+ Assertions.assertTrue(second.isPresent());
+ Assertions.assertEquals(interestPaymentWaiverAllocation, second.get());
+ }
+
private String createLoanJSON(AdvancedPaymentData... advancedPaymentData) {
return new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4").withRepaymentAfterEvery("1")
.withRepaymentTypeAsMonth().withinterestRatePerPeriod("1")
@@ -267,7 +323,20 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests {
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_INTEREST,
PaymentAllocationType.DUE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
+ advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
+ return advancedPaymentData;
+ }
+ private AdvancedPaymentData createInterestPaymentWaiverAllocation() {
+ AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
+ advancedPaymentData.setTransactionType("INTEREST_PAYMENT_WAIVER");
+ advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
+
+ List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_FEE,
+ PaymentAllocationType.PAST_DUE_PENALTY, PaymentAllocationType.PAST_DUE_INTEREST, PaymentAllocationType.PAST_DUE_PRINCIPAL,
+ PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_INTEREST,
+ PaymentAllocationType.DUE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
+ PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionInterestPaymentWaiverTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionInterestPaymentWaiverTest.java
new file mode 100644
index 000000000..843fb73be
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionInterestPaymentWaiverTest.java
@@ -0,0 +1,1767 @@
+/**
+ * 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;
+
+import static org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder.DEFAULT_STRATEGY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import org.apache.fineract.batch.command.internal.CreateTransactionLoanCommandStrategy;
+import org.apache.fineract.batch.domain.BatchRequest;
+import org.apache.fineract.batch.domain.BatchResponse;
+import org.apache.fineract.client.models.AdvancedPaymentData;
+import org.apache.fineract.client.models.BusinessDateRequest;
+import org.apache.fineract.client.models.GetLoansLoanIdLoanChargeData;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
+import org.apache.fineract.client.models.PaymentAllocationOrder;
+import org.apache.fineract.client.models.PostChargesRequest;
+import org.apache.fineract.client.models.PostChargesResponse;
+import org.apache.fineract.client.models.PostClientsResponse;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+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.PostLoansLoanIdTransactionsRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
+import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.client.models.PostLoansResponse;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.integrationtests.common.BatchHelper;
+import org.apache.fineract.integrationtests.common.BusinessDateHelper;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
+import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.accounting.Account;
+import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
+import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
+import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
+import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.apache.fineract.integrationtests.common.system.CodeHelper;
+import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
+import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
+import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
+import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
+import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
+import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
+import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(LoanTestLifecycleExtension.class)
+public class LoanTransactionInterestPaymentWaiverTest extends BaseLoanIntegrationTest {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AdvancedPaymentAllocationLoanRepaymentScheduleTest.class);
+ private static final String DATETIME_PATTERN = "dd MMMM yyyy";
+ private static ResponseSpecification responseSpec;
+ private static RequestSpecification requestSpec;
+ private static BusinessDateHelper businessDateHelper;
+ private static LoanTransactionHelper loanTransactionHelper;
+ private static AccountHelper accountHelper;
+ private static Integer commonLoanProductId;
+ private static PostClientsResponse client;
+ private static LoanRescheduleRequestHelper loanRescheduleRequestHelper;
+ private static ChargesHelper chargesHelper;
+
+ @BeforeAll
+ public static void setup() {
+ Utils.initializeRESTAssured();
+ requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+ requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+ requestSpec.header("Fineract-Platform-TenantId", "default");
+ responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+ loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
+ businessDateHelper = new BusinessDateHelper();
+ accountHelper = new AccountHelper(requestSpec, responseSpec);
+ ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
+ loanRescheduleRequestHelper = new LoanRescheduleRequestHelper(requestSpec, responseSpec);
+ chargesHelper = new ChargesHelper();
+
+ final Account assetAccount = accountHelper.createAssetAccount();
+ final Account incomeAccount = accountHelper.createIncomeAccount();
+ final Account expenseAccount = accountHelper.createExpenseAccount();
+ final Account overpaymentAccount = accountHelper.createLiabilityAccount();
+
+ commonLoanProductId = createLoanProduct("500", "15", "4", true, "25", true, LoanScheduleType.PROGRESSIVE,
+ LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
+ client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+ }
+
+ @Test
+ public void testInterestPaymentWaiverTransactionForProgressiveLoan() {
+ runAt("15 January 2023", () -> {
+ Integer numberOfRepayments = 4;
+ double amount = 1000.0;
+ String loanDisbursementDate = "1 January 2023";
+
+ Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
+ PostLoanProductsResponse loanProductResponse = loanProductHelper
+ .createLoanProduct(createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+ .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()));
+
+ Long loanId = applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(clientId,
+ loanProductResponse.getResourceId(), numberOfRepayments, loanDisbursementDate, amount, null);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 February 2023"), //
+ installment(250.0, false, "01 March 2023"), //
+ installment(250.0, false, "01 April 2023"), //
+ installment(250.0, false, "01 May 2023") //
+ );
+
+ loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2023")
+ .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0));
+
+ Long repayment1TransactionId = addInterestPaymentWaiverForLoan(loanId, 250.0, "2 January 2023");
+
+ assertNotNull(repayment1TransactionId);
+
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
+ transaction(250.0, "Interest Payment Waiver", "02 January 2023", 750.0, 250.0, 0.0, 0.0, 0, 0.0, 0.0));
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, 0, 0, 0, 0.0, true, "01 February 2023", 750.0), //
+ installment(250.0, false, "01 March 2023"), //
+ installment(250.0, false, "01 April 2023"), //
+ installment(250.0, false, "01 May 2023") //
+ );
+
+ Long repayment2TransactionId = addInterestPaymentWaiverForLoan(loanId, 250.0, "3 January 2023");
+
+ assertNotNull(repayment2TransactionId);
+
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
+ transaction(250.0, "Interest Payment Waiver", "02 January 2023", 750.0, 250.0, 0.0, 0.0, 0, 0.0, 0.0),
+ transaction(250.0, "Interest Payment Waiver", "03 January 2023", 500.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0));
+
+ });
+ }
+
+ @Test
+ public void testInterestPaymentWaiverTransactionValidationErrorTests() {
+ runAt("15 January 2023", () -> {
+ Integer numberOfRepayments = 4;
+ double amount = 1000.0;
+ String loanDisbursementDate = "1 January 2023";
+
+ Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
+ PostLoanProductsResponse loanProductResponse = loanProductHelper
+ .createLoanProduct(createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+ .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()));
+
+ Long loanId = applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(clientId,
+ loanProductResponse.getResourceId(), numberOfRepayments, loanDisbursementDate, amount, null);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 February 2023"), //
+ installment(250.0, false, "01 March 2023"), //
+ installment(250.0, false, "01 April 2023"), //
+ installment(250.0, false, "01 May 2023") //
+ );
+
+ // loan should be active
+ assertThrows(Exception.class, () -> addInterestPaymentWaiverForLoan(loanId, 250.0, "2 January 2023"));
+
+ loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2023")
+ .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+ // transaction cant be made before disbursement
+ assertThrows(Exception.class, () -> addInterestPaymentWaiverForLoan(loanId, 250.0, "30 December 2022"));
+ });
+ }
+
+ @Test
+ public void testInterestPaymentWaiverTransactionForProgressiveLoanWithExternalTransactionId() {
+ runAt("15 January 2023", () -> {
+ Integer numberOfRepayments = 4;
+ double amount = 1000.0;
+ String loanDisbursementDate = "1 January 2023";
+
+ Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
+ PostLoanProductsResponse loanProductResponse = loanProductHelper
+ .createLoanProduct(createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+ .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()));
+
+ String loanExternalIdStr = UUID.randomUUID().toString();
+ Long loanId = applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(clientId,
+ loanProductResponse.getResourceId(), numberOfRepayments, loanDisbursementDate, amount, loanExternalIdStr);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 February 2023"), //
+ installment(250.0, false, "01 March 2023"), //
+ installment(250.0, false, "01 April 2023"), //
+ installment(250.0, false, "01 May 2023") //
+ );
+
+ loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2023")
+ .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0));
+
+ // Check whether the provided external id was retrieved
+ String transactionExternalIdStr = UUID.randomUUID().toString();
+ final PostLoansLoanIdTransactionsResponse interestPaymentWaiverResultWithExternalId = loanTransactionHelper
+ .makeGoodwillCredit(loanExternalIdStr, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy")
+ .transactionDate("03 January 2023").locale("en").transactionAmount(5.0).externalId(transactionExternalIdStr));
+ assertEquals(transactionExternalIdStr, interestPaymentWaiverResultWithExternalId.getResourceExternalId());
+
+ GetLoansLoanIdTransactionsTransactionIdResponse response = loanTransactionHelper.getLoanTransactionDetails(loanId,
+ transactionExternalIdStr);
+ assertEquals(transactionExternalIdStr, response.getExternalId());
+ response = loanTransactionHelper.getLoanTransactionDetails(loanExternalIdStr,
+ interestPaymentWaiverResultWithExternalId.getResourceId());
+ assertEquals(transactionExternalIdStr, response.getExternalId());
+ response = loanTransactionHelper.getLoanTransactionDetails(loanExternalIdStr, transactionExternalIdStr);
+ assertEquals(transactionExternalIdStr, response.getExternalId());
+ });
+ }
+
+ @Test
+ public void testInterestPaymentWaiverTransactionReversePaymentForProgressiveLoan() {
+ runAt("15 January 2023", () -> {
+ Integer numberOfRepayments = 4;
+ double amount = 1000.0;
+ String loanDisbursementDate = "1 January 2023";
+
+ Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
+ PostLoanProductsResponse loanProductResponse = loanProductHelper
+ .createLoanProduct(createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+ .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()));
+
+ Long loanId = applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(clientId,
+ loanProductResponse.getResourceId(), numberOfRepayments, loanDisbursementDate, amount, null);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 February 2023"), //
+ installment(250.0, false, "01 March 2023"), //
+ installment(250.0, false, "01 April 2023"), //
+ installment(250.0, false, "01 May 2023") //
+ );
+
+ loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2023")
+ .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0));
+
+ Long repayment1TransactionId = addInterestPaymentWaiverForLoan(loanId, 250.0, "2 January 2023");
+
+ assertNotNull(repayment1TransactionId);
+
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
+ transaction(250.0, "Interest Payment Waiver", "02 January 2023", 750.0, 250.0, 0.0, 0.0, 0, 0.0, 0.0));
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, 0, 0, 0, 0.0, true, "01 February 2023", 750.0), //
+ installment(250.0, false, "01 March 2023"), //
+ installment(250.0, false, "01 April 2023"), //
+ installment(250.0, false, "01 May 2023") //
+ );
+
+ loanTransactionHelper.reverseRepayment(Math.toIntExact(loanId), Math.toIntExact(repayment1TransactionId), "2 January 2023");
+
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
+ transaction(250.0, "Interest Payment Waiver", "02 January 2023", 750.0, 250.0, 0.0, 0.0, 0, 0.0, 0.0, true));
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, 0, 0, 0, 250.0, false, "01 February 2023", 750.0), //
+ installment(250.0, false, "01 March 2023"), //
+ installment(250.0, false, "01 April 2023"), //
+ installment(250.0, false, "01 May 2023") //
+ );
+
+ });
+ }
+
+ @Test
+ public void testInterestPaymentWaiverTransactionChargeBackForProgressiveLoan() {
+ runAt("15 January 2023", () -> {
+ Integer numberOfRepayments = 4;
+ double amount = 1000.0;
+ String loanDisbursementDate = "1 January 2023";
+
+ Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
+ PostLoanProductsResponse loanProductResponse = loanProductHelper
+ .createLoanProduct(createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+ .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()));
+
+ Long loanId = applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(clientId,
+ loanProductResponse.getResourceId(), numberOfRepayments, loanDisbursementDate, amount, null);
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 February 2023"), //
+ installment(250.0, false, "01 March 2023"), //
+ installment(250.0, false, "01 April 2023"), //
+ installment(250.0, false, "01 May 2023") //
+ );
+
+ loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2023")
+ .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0));
+
+ Long repayment1TransactionId = addInterestPaymentWaiverForLoan(loanId, 250.0, "2 January 2023");
+
+ assertNotNull(repayment1TransactionId);
+
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
+ transaction(250.0, "Interest Payment Waiver", "02 January 2023", 750.0, 250.0, 0.0, 0.0, 0, 0.0, 0.0));
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, 0, 0, 0, 0.0, true, "01 February 2023", 750.0), //
+ installment(250.0, false, "01 March 2023"), //
+ installment(250.0, false, "01 April 2023"), //
+ installment(250.0, false, "01 May 2023") //
+ );
+
+ addChargebackForLoan(loanId, repayment1TransactionId, 250.0);
+
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
+ transaction(250.0, "Interest Payment Waiver", "02 January 2023", 750.0, 250.0, 0.0, 0.0, 0, 0.0, 0.0),
+ transaction(250.0, "Chargeback", "15 January 2023", 1000.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0));
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(500.0, 0, 0, 0, 250.0, false, "01 February 2023", 750.0), //
+ installment(250.0, false, "01 March 2023"), //
+ installment(250.0, false, "01 April 2023"), //
+ installment(250.0, false, "01 May 2023") //
+ );
+
+ });
+ }
+
+ private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
+ Integer numberOfRepayments, String loanDisbursementDate, double amount, String externalLoanId) {
+ LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
+ PostLoansRequest applicationRequest = applyLoanRequestProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(clientId,
+ loanProductId, externalLoanId, amount, numberOfRepayments, loanDisbursementDate);
+
+ PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
+
+ Long loanId = loanResponse.getLoanId();
+
+ assertNotNull(loanId);
+
+ loanTransactionHelper.approveLoan(loanId, new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(amount))
+ .dateFormat(DATETIME_PATTERN).approvedOnDate(loanDisbursementDate).locale("en"));
+
+ return loanId;
+ }
+
+ private PostLoansRequest applyLoanRequestProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanId,
+ String loanExternalId, double amount, Integer numberOfRepayments, String loanDisbursementDate) {
+ PostLoansRequest postLoansRequest = new PostLoansRequest().clientId(clientId).productId(loanId)
+ .submittedOnDate(loanDisbursementDate).expectedDisbursementDate(loanDisbursementDate).dateFormat(DATETIME_PATTERN)
+ .locale("en").loanType("individual")
+ .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).amortizationType(1)
+ .interestRatePerPeriod(BigDecimal.ZERO).interestCalculationPeriodType(1).interestType(0)
+ .maxOutstandingLoanBalance(BigDecimal.valueOf(amount)).principal(BigDecimal.valueOf(amount))
+ .loanTermFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequency(numberOfRepayments)
+ .repaymentFrequencyType(RepaymentFrequencyType.MONTHS).repaymentEvery(1).numberOfRepayments(numberOfRepayments);
+ if (loanExternalId != null) {
+ postLoansRequest.externalId(loanExternalId);
+ }
+ return postLoansRequest;
+ }
+
+ // PW UC3: Overpayment
+ // ADVANCED_PAYMENT_ALLOCATION_STRATEGY
+ // 1. Disburse the loan
+ // 2. Pay down payment
+ // 3. Overpay 2nd installment
+ @Test
+ public void testInterestPaymentWaiverUC3() {
+ runAt("15 February 2023", () -> {
+
+ final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId,
+ BigDecimal.valueOf(500.0), 45, 15, 3, BigDecimal.ZERO, "01 January 2023", "01 January 2023");
+
+ loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN)
+ .approvedOnDate("01 January 2023").locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN)
+ .transactionAmount(BigDecimal.valueOf(500.00)).locale("en"));
+
+ GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeInterestPaymentWaiver(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(150.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 225.0, 275.0, 225.0, 275.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 25.0, 100.0, 25.0, 0.0);
+ validateLoanTransaction(loanDetails, 2, 150.0, 150.0, 0.0, 225.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(125.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 100.0, 400.0, 100.0, 400.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 25.0, 100.0, 25.0, 0.0);
+ validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 100.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(125.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, 25.0);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 25.0, 0.0);
+ validateLoanTransaction(loanDetails, 4, 125.0, 100.0, 25.0, 0.0);
+ assertTrue(loanDetails.getStatus().getOverpaid());
+ });
+ }
+
+ // PW UC12: Refund last installment
+ // ADVANCED_PAYMENT_ALLOCATION_STRATEGY
+ // 1. Disburse the loan
+ // 2. Pay down payment
+ // 3. Interest Payment Waiver in advance
+ // 4. Pay rest on time
+ @Test
+ public void testInterestPaymentWaiverUC12() {
+ runAt("15 February 2023", () -> {
+
+ final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId,
+ BigDecimal.valueOf(500.0), 45, 15, 3, BigDecimal.ZERO, "01 January 2023", "01 January 2023");
+
+ loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN)
+ .approvedOnDate("01 January 2023").locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN)
+ .transactionAmount(BigDecimal.valueOf(500.00)).locale("en"));
+
+ GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeInterestPaymentWaiver(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("08 January 2023").locale("en").transactionAmount(200.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 175.0, 325.0, 175.0, 325.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 75.0, 50.0, 75.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateLoanTransaction(loanDetails, 2, 200.0, 200.0, 0.0, 175.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 50.0, 450.0, 50.0, 450.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 75.0, 50.0, 75.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 50.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(50.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 75.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 50.0);
+ assertTrue(loanDetails.getStatus().getClosedObligationsMet());
+ });
+ }
+
+ // PW UC13: Refund last installment
+ // ADVANCED_PAYMENT_ALLOCATION_STRATEGY
+ // 1. Disburse the loan
+ // 2. Pay down payment
+ // 3. InterestPaymentWaiver in advance
+ // 4. Pay rest on time
+ @Test
+ public void testInterestPaymentWaiverUC13() {
+ runAt("15 February 2023", () -> {
+
+ final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId,
+ BigDecimal.valueOf(500.0), 45, 15, 3, BigDecimal.ZERO, "01 January 2023", "01 January 2023");
+
+ loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN)
+ .approvedOnDate("01 January 2023").locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN)
+ .transactionAmount(BigDecimal.valueOf(500.00)).locale("en"));
+
+ GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeInterestPaymentWaiver(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("08 January 2023").locale("en").transactionAmount(200.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 175.0, 325.0, 175.0, 325.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 75.0, 50.0, 75.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateLoanTransaction(loanDetails, 2, 200.0, 200.0, 0.0, 175.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 50.0, 450.0, 50.0, 450.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 75.0, 50.0, 75.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 50.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(50.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 75.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 50.0);
+ assertTrue(loanDetails.getStatus().getClosedObligationsMet());
+ });
+ }
+
+ // PW UC15: Interest Payment Waiver PD
+ // ADVANCED_PAYMENT_ALLOCATION_STRATEGY
+ // 1. Disburse the loan
+ // 2. Pay down payment
+ // 3. Pay first installment on time - fails
+ // 4. Refund (DP due and in advance)
+ // 5. Pay rest on time
+ @Test
+ public void testInterestPaymentWaiverUC15() {
+ runAt("15 February 2023", () -> {
+
+ final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId,
+ BigDecimal.valueOf(500.0), 45, 15, 3, BigDecimal.ZERO, "01 January 2023", "01 January 2023");
+
+ loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN)
+ .approvedOnDate("01 January 2023").locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN)
+ .transactionAmount(BigDecimal.valueOf(500.00)).locale("en"));
+
+ GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(1).getId(),
+ new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("01 January 2023")
+ .transactionAmount(0.0).locale("en"));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeInterestPaymentWaiver(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("15 January 2023").locale("en").transactionAmount(200.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 300.0, 200.0, 300.0, 200.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 75.0, 50.0, 75.0, 0.0);
+ validateLoanTransaction(loanDetails, 2, 200.0, 200.0, 0.0, 300.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0));
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 175.0, 325.0, 175.0, 325.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 75.0, 50.0, 75.0, 0.0);
+ validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 175.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(125.0));
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 50.0, 450.0, 50.0, 450.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 75.0, 50.0, 75.0, 0.0);
+ validateLoanTransaction(loanDetails, 4, 125.0, 125.0, 0.0, 50.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(50.0));
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 75.0, 0.0);
+ validateLoanTransaction(loanDetails, 5, 50.0, 50.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getClosedObligationsMet());
+ });
+ }
+
+ // PW UC17c: Full refund with CBR
+ // ADVANCED_PAYMENT_ALLOCATION_STRATEGY
+ // 1. Disburse the loan
+ // 2. Pay down payment
+ // 3. Pay first installment on time
+ // 4. Full Goodwill credit
+ // 5. CBR
+ @Test
+ public void testInterestPaymentWaiverUC17c() {
+ runAt("15 February 2023", () -> {
+
+ final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId,
+ BigDecimal.valueOf(500.0), 45, 15, 3, BigDecimal.ZERO, "01 January 2023", "01 January 2023");
+
+ loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN)
+ .approvedOnDate("01 January 2023").locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN)
+ .transactionAmount(BigDecimal.valueOf(500.00)).locale("en"));
+
+ GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0);
+ validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeInterestPaymentWaiver(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("08 January 2023").locale("en").transactionAmount(500.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, 125.0);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateLoanTransaction(loanDetails, 2, 500.0, 375.0, 125.0, 0.0);
+ assertTrue(loanDetails.getStatus().getOverpaid());
+
+ loanTransactionHelper.makeCreditBalanceRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("09 January 2023").locale("en").transactionAmount(125.0));
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null);
+ validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0);
+ validateLoanTransaction(loanDetails, 3, 125.0, 0.0, 125.0, 0.0);
+ assertTrue(loanDetails.getStatus().getClosedObligationsMet());
+ });
+ }
+
+ /**
+ * Tests successful run of batch Interest Payment Waiver for loans. 200(OK) status is returned for successful
+ * responses. It first creates a new loan, approves and disburses the loan. Then a Interest Payment Waiver request
+ * is made
+ *
+ * @see CreateTransactionLoanCommandStrategy
+ */
+ @Test
+ public void shouldReturnOkStatusForBatchInterestPaymentWaiver() {
+
+ final String loanProductJSON = new LoanProductTestBuilder() //
+ .withPrincipal("1000.00") //
+ .withNumberOfRepayments("24") //
+ .withRepaymentAfterEvery("1") //
+ .withRepaymentTypeAsMonth() //
+ .withinterestRatePerPeriod("2") //
+ .withInterestRateFrequencyTypeAsMonths() //
+ .withAmortizationTypeAsEqualPrincipalPayment() //
+ .withInterestTypeAsDecliningBalance() //
+ .currencyDetails("0", "100").build(null);
+
+ final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec);
+ ClientHelper.verifyClientCreatedOnServer(requestSpec, responseSpec, clientID);
+
+ final Integer collateralId = CollateralManagementHelper.createCollateralProduct(requestSpec, responseSpec);
+ Assertions.assertNotNull(collateralId);
+ final Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(requestSpec, responseSpec, clientID.toString(),
+ collateralId);
+ Assertions.assertNotNull(clientCollateralId);
+
+ final Integer productId = new LoanTransactionHelper(requestSpec, responseSpec).getLoanProductId(loanProductJSON);
+
+ final Long createActiveClientRequestId = 4730L;
+ final Long applyLoanRequestId = createActiveClientRequestId + 1;
+ final Long approveLoanRequestId = applyLoanRequestId + 1;
+ final Long disburseLoanRequestId = approveLoanRequestId + 1;
+ final Long interestPaymentWaiverRequestId = disburseLoanRequestId + 1;
+
+ // Create a createClient Request
+ final BatchRequest br1 = BatchHelper.createActiveClientRequest(createActiveClientRequestId, "");
+
+ // Create a ApplyLoan Request
+ final BatchRequest br2 = BatchHelper.applyLoanRequest(applyLoanRequestId, createActiveClientRequestId, productId,
+ clientCollateralId);
+
+ // Create a approveLoan Request
+ final BatchRequest br3 = BatchHelper.approveLoanRequest(approveLoanRequestId, applyLoanRequestId);
+
+ // Create a disburseLoan Request
+ final BatchRequest br4 = BatchHelper.disburseLoanRequest(disburseLoanRequestId, approveLoanRequestId);
+
+ // Create a Interest Payment Waiver request.
+ final BatchRequest br5 = BatchHelper.interestPaymentWaiverRequest(interestPaymentWaiverRequestId, disburseLoanRequestId, "500");
+
+ final List<BatchRequest> batchRequests = new ArrayList<>();
+
+ batchRequests.add(br1);
+ batchRequests.add(br2);
+ batchRequests.add(br3);
+ batchRequests.add(br4);
+ batchRequests.add(br5);
+
+ final String batchRequestStr = BatchHelper.toJsonString(batchRequests);
+
+ final List<BatchResponse> response = BatchHelper.postBatchRequestsWithoutEnclosingTransaction(requestSpec, responseSpec,
+ batchRequestStr);
+
+ Assertions.assertEquals(HttpStatus.SC_OK, (long) response.get(4).getStatusCode(), "Verify Status Code 200 for Goodwill credit");
+ }
+
+ // PW UC112: Advanced payment allocation, horizontal repayment processing
+ // ADVANCED_PAYMENT_ALLOCATION_STRATEGY
+ // 1. Disburse the loan (1000)
+ // 2. Add charge after maturity date
+ // 3. Pay 1st installment
+ // 4. Pay 2nd installment
+ // 5. Add charge to 3rd installment
+ // 6. Add charge to 4th installment
+ // 7. Do Interest Payment Waiver (in advance payment)
+ @Test
+ public void testInterestPaymentWaiverUC112() {
+ runAt("01 September 2023", () -> {
+
+ final Account assetAccount = accountHelper.createAssetAccount();
+ final Account incomeAccount = accountHelper.createIncomeAccount();
+ final Account expenseAccount = accountHelper.createExpenseAccount();
+ final Account overpaymentAccount = accountHelper.createLiabilityAccount();
+ Integer localLoanProductId = createLoanProduct("1000", "15", "3", true, "25", false, LoanScheduleType.PROGRESSIVE,
+ LoanScheduleProcessingType.HORIZONTAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
+ final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId,
+ BigDecimal.valueOf(1000.0), 45, 15, 3, BigDecimal.ZERO, "01 September 2023", "01 September 2023");
+
+ loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN)
+ .approvedOnDate("01 September 2023").locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().actualDisbursementDate("01 September 2023").dateFormat(DATETIME_PATTERN)
+ .transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+ GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 1000.0, 0.0, 1000.0, 0.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ // Add Charge Penalty
+ Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec,
+ ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", true));
+ loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(),
+ LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 October 2023", "20"));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 1020.0, 0.0, 1000.0, 0.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("01 September 2023").locale("en").transactionAmount(250.0));
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 770.0, 250.0, 750.0, 250.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
+ .date("2023.09.16").dateFormat("yyyy.MM.dd").locale("en"));
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(250.0));
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 520.0, 500.0, 500.0, 500.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(),
+ LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 September 2023", "20"));
+ loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(),
+ LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "16 October 2023", "20"));
+
+ loanTransactionHelper.makeInterestPaymentWaiver(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(50.0));
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 510.0, 550.0, 490.0, 510.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 10.0, 240.0, 0.0, 0.0, 0.0, 20.0, 20.0, 0.0, 0.0,
+ 0.0, 0.0, 30.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0, 0.0, 0.0, 0.0,
+ 0.0, 20.0, 0.0);
+
+ validateLoanCharge(loanDetails, 0, LocalDate.of(2023, 9, 17), 20.0, 0.0, 20.0);
+ validateLoanCharge(loanDetails, 1, LocalDate.of(2023, 10, 16), 20.0, 20.0, 0.0);
+ validateLoanCharge(loanDetails, 2, LocalDate.of(2023, 10, 17), 20.0, 20.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+ });
+ }
+
+ // PW UC113: Advanced payment allocation, vertical repayment processing
+ // ADVANCED_PAYMENT_ALLOCATION_STRATEGY
+ // 1. Disburse the loan (1000)
+ // 2. Add charge after maturity date
+ // 3. Pay 1st installment
+ // 4. Pay 2nd installment
+ // 5. Add charge to 3rd installment
+ // 6. Add charge to 4th installment
+ // 7. Do Interest Payment Waiver (in advance payment)
+ @Test
+ public void testInterestPaymentWaiverUC113() {
+ runAt("01 September 2023", () -> {
+
+ final Account assetAccount = accountHelper.createAssetAccount();
+ final Account incomeAccount = accountHelper.createIncomeAccount();
+ final Account expenseAccount = accountHelper.createExpenseAccount();
+ final Account overpaymentAccount = accountHelper.createLiabilityAccount();
+ Integer localLoanProductId = createLoanProduct("1000", "15", "3", true, "25", false, LoanScheduleType.PROGRESSIVE,
+ LoanScheduleProcessingType.VERTICAL, assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
+ final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), localLoanProductId,
+ BigDecimal.valueOf(1000.0), 45, 15, 3, BigDecimal.ZERO, "01 September 2023", "01 September 2023",
+ LoanScheduleProcessingType.VERTICAL);
+
+ loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN)
+ .approvedOnDate("01 September 2023").locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().actualDisbursementDate("01 September 2023").dateFormat(DATETIME_PATTERN)
+ .transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+ GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 1000.0, 0.0, 1000.0, 0.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ // Add Charge Penalty
+ Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec,
+ ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", true));
+ loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(),
+ LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 October 2023", "20"));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 1020.0, 0.0, 1000.0, 0.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("01 September 2023").locale("en").transactionAmount(250.0));
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 770.0, 250.0, 750.0, 250.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
+ .date("2023.09.16").dateFormat("yyyy.MM.dd").locale("en"));
+
+ loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(250.0));
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 520.0, 500.0, 500.0, 500.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0, 20.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(),
+ LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "17 September 2023", "20"));
+ loanTransactionHelper.addChargesForLoan(loanResponse.getLoanId().intValue(),
+ LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), "16 October 2023", "20"));
+
+ loanTransactionHelper.makeInterestPaymentWaiver(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate("16 September 2023").locale("en").transactionAmount(50.0));
+ loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ validateLoanSummaryBalances(loanDetails, 510.0, 550.0, 500.0, 500.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 9, 1), 250.0, 250.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 9, 16), 250.0, 250.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 10, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 20.0, 10.0, 10.0, 0.0, 0.0,
+ 0.0, 10.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 10, 16), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 20.0, 20.0, 0.0, 0.0, 0.0,
+ 0.0, 20.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2023, 10, 17), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0, 0.0, 0.0, 0.0,
+ 0.0, 20.0, 0.0);
+ validateLoanCharge(loanDetails, 0, LocalDate.of(2023, 9, 17), 20.0, 10.0, 10.0);
+ validateLoanCharge(loanDetails, 1, LocalDate.of(2023, 10, 16), 20.0, 20.0, 0.0);
+ validateLoanCharge(loanDetails, 2, LocalDate.of(2023, 10, 17), 20.0, 20.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+ });
+ }
+
+ // Create Loan with Interest for Accural accounting
+ // Approve and disburse loan
+ // charge penalty with due date as 1st installment
+ // charge fee with due date as 1st installment
+ // charge penalty with due date as 3rd installment
+ // charge fee with due date as 2nd installment
+ // pay 1st-3rd installment on time by Interest Payment Waiver
+ // pay 4th (last) installment on time with overpayment by Interest Payment Waiver
+ // reverse 4 - 1 installment Payment Waiver transactions
+ @Test
+ public void testAccounting() {
+ runAt("15 May 2023", () -> {
+
+ final String disbursementDay = "01 January 2023";
+ final String repaymentPeriod1DueDate = "01 February 2023";
+ final String repaymentPeriod2DueDate = "01 March 2023";
+ final String repaymentPeriod3DueDate = "01 April 2023";
+ final String repaymentPeriod4DueDate = "01 May 2023";
+
+ Long localLoanProductId = createLoanProductAccountingAccuralPeriodicWithInterest();
+ final Long loanId = applyForLoanApplicationWithInterest(client.getClientId(), localLoanProductId, BigDecimal.valueOf(40000),
+ disbursementDay);
+
+ loanTransactionHelper.approveLoan(loanId, new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000))
+ .dateFormat(DATETIME_PATTERN).approvedOnDate(disbursementDay).locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay)
+ .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+ chargePenalty(loanId, 30.0, repaymentPeriod1DueDate);
+ chargePenalty(loanId, 50.0, repaymentPeriod2DueDate);
+ chargeFee(loanId, 40.0, repaymentPeriod1DueDate);
+ chargeFee(loanId, 60.0, repaymentPeriod3DueDate);
+
+ GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 1260.0, 0.0, 1000.0, 0.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 0.0, 250.0, 40.0, 0.0, 40.0, 30.0, 0.0, 30.0, 20.0,
+ 0.0, 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 50.0, 0.0, 50.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ // transaction 1
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr1 = loanTransactionHelper.makeInterestPaymentWaiver(loanId,
+ new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod1DueDate)
+ .locale("en").transactionAmount(340.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 920.0, 340.0, 750.0, 250.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 50.0, 0.0, 50.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr1.getResourceId(), journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(30, penaltyReceivableAccount, "CREDIT"), //
+ journalEntry(40, feeReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(340.0, interestIncomeAccount, "DEBIT"));
+
+ verifyJournalEntries(loanId, journalEntry(1000.0, fundSource, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(30, penaltyReceivableAccount, "CREDIT"), //
+ journalEntry(40, feeReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(340.0, interestIncomeAccount, "DEBIT"));
+
+ // transaction 2
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr2 = loanTransactionHelper.makeInterestPaymentWaiver(loanId,
+ new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod2DueDate)
+ .locale("en").transactionAmount(320.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 600.0, 660.0, 500.0, 500.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 50.0, 50.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr2.getResourceId(), journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(50, penaltyReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(320.0, interestIncomeAccount, "DEBIT"));
+
+ verifyJournalEntries(loanId, journalEntry(1000.0, fundSource, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(30, penaltyReceivableAccount, "CREDIT"), //
+ journalEntry(40, feeReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(320.0, interestIncomeAccount, "DEBIT"), //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(50, penaltyReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(340.0, interestIncomeAccount, "DEBIT"));
+
+ // transaction 3
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr3 = loanTransactionHelper.makeInterestPaymentWaiver(loanId,
+ new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod3DueDate)
+ .locale("en").transactionAmount(330.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 270.0, 990.0, 250.0, 750.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 50.0, 50.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 250.0, 0.0, 60.0, 60.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr3.getResourceId(), journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(60, feeReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(330.0, interestIncomeAccount, "DEBIT"));
+
+ verifyJournalEntries(loanId, journalEntry(1000.0, fundSource, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(30, penaltyReceivableAccount, "CREDIT"), //
+ journalEntry(40, feeReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(320.0, interestIncomeAccount, "DEBIT"), //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(50, penaltyReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(340.0, interestIncomeAccount, "DEBIT"), //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(60, feeReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(330.0, interestIncomeAccount, "DEBIT"));
+
+ // transaction 4 + overpayment
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr4 = loanTransactionHelper.makeInterestPaymentWaiver(loanId,
+ new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod4DueDate)
+ .locale("en").transactionAmount(350.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 0.0, 1260.0, 0.0, 1000.0, 80.0);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 50.0, 50.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 250.0, 0.0, 60.0, 60.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ assertFalse(loanDetails.getStatus().getActive());
+
+ // Because of closing and other reason we skip validating loan's journal entries
+
+ verifyTRJournalEntries(interestPaymentWaiverTr4.getResourceId(), journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(80, overpaymentAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(350.0, interestIncomeAccount, "DEBIT"));
+
+ // reverse transaction 4
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr4Reverse = loanTransactionHelper.reverseLoanTransaction(loanId,
+ interestPaymentWaiverTr4.getResourceId(), new PostLoansLoanIdTransactionsTransactionIdRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod4DueDate).transactionAmount(0.0).locale("en"));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 270.0, 990.0, 250.0, 750.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 50.0, 50.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 250.0, 0.0, 60.0, 60.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr4Reverse.getResourceId(), journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(80, overpaymentAccount, "DEBIT"), //
+ journalEntry(20, interestReceivableAccount, "DEBIT"), //
+ journalEntry(350.0, interestIncomeAccount, "CREDIT"), //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(80, overpaymentAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(350.0, interestIncomeAccount, "DEBIT"));
+
+ // reverse transaction 3
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr3Reverse = loanTransactionHelper.reverseLoanTransaction(loanId,
+ interestPaymentWaiverTr3.getResourceId(), new PostLoansLoanIdTransactionsTransactionIdRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod3DueDate).transactionAmount(0.0).locale("en"));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 600.0, 660.0, 500.0, 500.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 50.0, 50.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr3Reverse.getResourceId(), journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(60, feeReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(330.0, interestIncomeAccount, "DEBIT"), //
+
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(60, feeReceivableAccount, "DEBIT"), //
+ journalEntry(20, interestReceivableAccount, "DEBIT"), //
+ journalEntry(330.0, interestIncomeAccount, "CREDIT"));
+
+ // reverse transaction 2
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr2Reverse = loanTransactionHelper.reverseLoanTransaction(loanId,
+ interestPaymentWaiverTr2.getResourceId(), new PostLoansLoanIdTransactionsTransactionIdRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod2DueDate).transactionAmount(0.0).locale("en"));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 920.0, 340.0, 750.0, 250.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 50.0, 0.0, 50.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr2Reverse.getResourceId(), journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(50, penaltyReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(320.0, interestIncomeAccount, "DEBIT"), //
+
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(50, penaltyReceivableAccount, "DEBIT"), //
+ journalEntry(20, interestReceivableAccount, "DEBIT"), //
+ journalEntry(320.0, interestIncomeAccount, "CREDIT"));
+
+ // reverse transaction 1
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr1Reverse = loanTransactionHelper.reverseLoanTransaction(loanId,
+ interestPaymentWaiverTr1.getResourceId(), new PostLoansLoanIdTransactionsTransactionIdRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod1DueDate).transactionAmount(0.0).locale("en"));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 1260.0, 0.0, 1000.0, 0.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 0.0, 250.0, 40.0, 0.0, 40.0, 30.0, 0.0, 30.0, 20.0,
+ 0.0, 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 50.0, 0.0, 50.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr1Reverse.getResourceId(), journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(30, penaltyReceivableAccount, "CREDIT"), //
+ journalEntry(40, feeReceivableAccount, "CREDIT"), //
+ journalEntry(20, interestReceivableAccount, "CREDIT"), //
+ journalEntry(340.0, interestIncomeAccount, "DEBIT"), //
+
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(30, penaltyReceivableAccount, "DEBIT"), //
+ journalEntry(40, feeReceivableAccount, "DEBIT"), //
+ journalEntry(20, interestReceivableAccount, "DEBIT"), //
+ journalEntry(340.0, interestIncomeAccount, "CREDIT"));
+ });
+ }
+
+ // Create Loan with Interest for Accural accounting
+ // Approve and disburse loan
+ // charge penalty with due date as 1st installment
+ // charge fee with due date as 1st installment
+ // charge penalty with due date as 3rd installment
+ // charge fee with due date as 2nd installment
+ // Charge-OFF loan
+ // pay 1st-3rd installment on time by Interest Payment Waiver
+ // pay 4th (last) installment on time with overpayment by Interest Payment Waiver
+ // reverse 4 - 1 Interest Payment Waiver transactions
+ @Test
+ public void testInterestPaymentWaiverTransactionAccountingAccuralForInterestPenaltyFeeOverpaymentChargeOFFLoan() {
+ runAt("15 May 2023", () -> {
+
+ final String disbursementDay = "01 January 2023";
+ final String repaymentPeriod1DueDate = "01 February 2023";
+ final String repaymentPeriod2DueDate = "01 March 2023";
+ final String repaymentPeriod3DueDate = "01 April 2023";
+ final String repaymentPeriod4DueDate = "01 May 2023";
+
+ Long localLoanProductId = createLoanProductAccountingAccuralPeriodicWithInterest();
+ final Long loanId = applyForLoanApplicationWithInterest(client.getClientId(), localLoanProductId, BigDecimal.valueOf(40000),
+ disbursementDay);
+
+ loanTransactionHelper.approveLoan(loanId, new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000))
+ .dateFormat(DATETIME_PATTERN).approvedOnDate(disbursementDay).locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay)
+ .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+ chargePenalty(loanId, 30.0, repaymentPeriod1DueDate);
+ chargePenalty(loanId, 50.0, repaymentPeriod2DueDate);
+ chargeFee(loanId, 40.0, repaymentPeriod1DueDate);
+ chargeFee(loanId, 60.0, repaymentPeriod3DueDate);
+
+ // Charge-OFF loan
+ String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6)
+ + Utils.randomStringGenerator("is", 5);
+ Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
+ String transactionExternalId = UUID.randomUUID().toString();
+ PostLoansLoanIdTransactionsResponse chargeOffTransaction = loanTransactionHelper.chargeOffLoan(loanId,
+ new PostLoansLoanIdTransactionsRequest().transactionDate("2 January 2023").locale("en").dateFormat("dd MMMM yyyy")
+ .externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));
+
+ GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 1260.0, 0.0, 1000.0, 0.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 0.0, 250.0, 40.0, 0.0, 40.0, 30.0, 0.0, 30.0, 20.0,
+ 0.0, 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 50.0, 0.0, 50.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ // transaction 1
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr1 = loanTransactionHelper.makeInterestPaymentWaiver(loanId,
+ new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod1DueDate)
+ .locale("en").transactionAmount(340.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 920.0, 340.0, 750.0, 250.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 50.0, 0.0, 50.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr1.getResourceId(), journalEntry(340.0, interestIncomeChargeOffAccount, "CREDIT"), //
+ journalEntry(340.0, interestIncomeAccount, "DEBIT"));
+
+ // transaction 2
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr2 = loanTransactionHelper.makeInterestPaymentWaiver(loanId,
+ new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod2DueDate)
+ .locale("en").transactionAmount(320.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 600.0, 660.0, 500.0, 500.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 50.0, 50.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr2.getResourceId(), journalEntry(320.0, interestIncomeChargeOffAccount, "CREDIT"), //
+ journalEntry(320.0, interestIncomeAccount, "DEBIT"));
+
+ // transaction 3
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr3 = loanTransactionHelper.makeInterestPaymentWaiver(loanId,
+ new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod3DueDate)
+ .locale("en").transactionAmount(330.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 270.0, 990.0, 250.0, 750.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 50.0, 50.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 250.0, 0.0, 60.0, 60.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr3.getResourceId(), journalEntry(330.0, interestIncomeChargeOffAccount, "CREDIT"), //
+ journalEntry(330.0, interestIncomeAccount, "DEBIT"));
+
+ // transaction 4 + overpayment
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr4 = loanTransactionHelper.makeInterestPaymentWaiver(loanId,
+ new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod4DueDate)
+ .locale("en").transactionAmount(350.0));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 0.0, 1260.0, 0.0, 1000.0, 80.0);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 50.0, 50.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 250.0, 0.0, 60.0, 60.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ assertFalse(loanDetails.getStatus().getActive());
+
+ // Because of closing and other reason we skip validating loan's journal entries
+
+ verifyTRJournalEntries(interestPaymentWaiverTr4.getResourceId(), journalEntry(270.0, interestIncomeChargeOffAccount, "CREDIT"), //
+ journalEntry(80, overpaymentAccount, "CREDIT"), //
+ journalEntry(350.0, interestIncomeAccount, "DEBIT"));
+
+ // reverse transaction 4
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr4Reverse = loanTransactionHelper.reverseLoanTransaction(loanId,
+ interestPaymentWaiverTr4.getResourceId(), new PostLoansLoanIdTransactionsTransactionIdRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod4DueDate).transactionAmount(0.0).locale("en"));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 270.0, 990.0, 250.0, 750.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 50.0, 50.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 250.0, 0.0, 60.0, 60.0, 0.0, 0.0, 0.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr4Reverse.getResourceId(),
+ journalEntry(270.0, interestIncomeChargeOffAccount, "CREDIT"), //
+ journalEntry(80, overpaymentAccount, "CREDIT"), //
+ journalEntry(350.0, interestIncomeAccount, "DEBIT"), //
+ journalEntry(270.0, interestIncomeChargeOffAccount, "DEBIT"), //
+ journalEntry(80, overpaymentAccount, "DEBIT"), //
+ journalEntry(350.0, interestIncomeAccount, "CREDIT"));
+
+ // reverse transaction 3
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr3Reverse = loanTransactionHelper.reverseLoanTransaction(loanId,
+ interestPaymentWaiverTr3.getResourceId(), new PostLoansLoanIdTransactionsTransactionIdRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod3DueDate).transactionAmount(0.0).locale("en"));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 600.0, 660.0, 500.0, 500.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 50.0, 50.0, 0.0, 20.0, 20.0,
+ 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr3Reverse.getResourceId(),
+ journalEntry(330.0, interestIncomeChargeOffAccount, "CREDIT"), //
+ journalEntry(330.0, interestIncomeAccount, "DEBIT"), //
+ journalEntry(330.0, interestIncomeChargeOffAccount, "DEBIT"), //
+ journalEntry(330.0, interestIncomeAccount, "CREDIT"));
+
+ // reverse transaction 2
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr2Reverse = loanTransactionHelper.reverseLoanTransaction(loanId,
+ interestPaymentWaiverTr2.getResourceId(), new PostLoansLoanIdTransactionsTransactionIdRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod2DueDate).transactionAmount(0.0).locale("en"));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 920.0, 340.0, 750.0, 250.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 250.0, 0.0, 40.0, 40.0, 0.0, 30.0, 30.0, 0.0, 20.0,
+ 20.0, 0.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 50.0, 0.0, 50.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr2Reverse.getResourceId(),
+ journalEntry(320.0, interestIncomeChargeOffAccount, "CREDIT"), //
+ journalEntry(320.0, interestIncomeAccount, "DEBIT"), //
+ journalEntry(320.0, interestIncomeChargeOffAccount, "DEBIT"), //
+ journalEntry(320.0, interestIncomeAccount, "CREDIT"));
+
+ // reverse transaction 1
+ PostLoansLoanIdTransactionsResponse interestPaymentWaiverTr1Reverse = loanTransactionHelper.reverseLoanTransaction(loanId,
+ interestPaymentWaiverTr1.getResourceId(), new PostLoansLoanIdTransactionsTransactionIdRequest()
+ .dateFormat(DATETIME_PATTERN).transactionDate(repaymentPeriod1DueDate).transactionAmount(0.0).locale("en"));
+
+ loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+ validateLoanSummaryBalances(loanDetails, 1260.0, 0.0, 1000.0, 0.0, null);
+ validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 2, 1), 250.0, 0.0, 250.0, 40.0, 0.0, 40.0, 30.0, 0.0, 30.0, 20.0,
+ 0.0, 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 3, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 50.0, 0.0, 50.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 4, 1), 250.0, 0.0, 250.0, 60.0, 0.0, 60.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 5, 1), 250.0, 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 20.0, 0.0,
+ 20.0, 0.0, 0.0);
+ assertTrue(loanDetails.getStatus().getActive());
+
+ verifyTRJournalEntries(interestPaymentWaiverTr1Reverse.getResourceId(),
+ journalEntry(340.0, interestIncomeChargeOffAccount, "CREDIT"), //
+ journalEntry(340.0, interestIncomeAccount, "DEBIT"), //
+ journalEntry(340.0, interestIncomeChargeOffAccount, "DEBIT"), //
+ journalEntry(340.0, interestIncomeAccount, "CREDIT"));
+ });
+ }
+
+ private void chargeFee(Long loanId, Double amount, String dueDate) {
+ PostChargesResponse feeCharge = chargesHelper.createCharges(new PostChargesRequest().penalty(false).amount(9.0)
+ .chargeCalculationType(ChargeCalculationType.FLAT.getValue()).chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue())
+ .chargePaymentMode(ChargePaymentMode.REGULAR.getValue()).currencyCode("USD")
+ .name(Utils.randomStringGenerator("FEE_" + Calendar.getInstance().getTimeInMillis(), 5)).chargeAppliesTo(1).locale("en")
+ .active(true));
+ PostLoansLoanIdChargesResponse feeLoanChargeResult = loanTransactionHelper.addChargesForLoan(loanId,
+ new PostLoansLoanIdChargesRequest().chargeId(feeCharge.getResourceId()).dateFormat(DATETIME_PATTERN).locale("en")
+ .amount(amount).dueDate(dueDate));
+ assertNotNull(feeLoanChargeResult);
+ assertNotNull(feeLoanChargeResult.getResourceId());
+ }
+
+ private void chargePenalty(Long loanId, Double amount, String dueDate) {
+ PostChargesResponse penaltyCharge = chargesHelper.createCharges(new PostChargesRequest().penalty(true).amount(10.0)
+ .chargeCalculationType(ChargeCalculationType.FLAT.getValue()).chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue())
+ .chargePaymentMode(ChargePaymentMode.REGULAR.getValue()).currencyCode("USD")
+ .name(Utils.randomStringGenerator("PENALTY_" + Calendar.getInstance().getTimeInMillis(), 5)).chargeAppliesTo(1).locale("en")
+ .active(true));
+ PostLoansLoanIdChargesResponse penaltyLoanChargeResult = loanTransactionHelper.addChargesForLoan(loanId,
+ new PostLoansLoanIdChargesRequest().chargeId(penaltyCharge.getResourceId()).dateFormat(DATETIME_PATTERN).locale("en")
+ .amount(amount).dueDate(dueDate));
+ assertNotNull(penaltyLoanChargeResult);
+ assertNotNull(penaltyLoanChargeResult.getResourceId());
+ }
+
+ private Long createLoanProductAccountingAccuralPeriodicWithInterest() {
+ LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
+ String name = Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6);
+ String shortName = Utils.uniqueRandomStringGenerator("", 4);
+ return loanTransactionHelper.createLoanProduct(new PostLoanProductsRequest().name(name).shortName(shortName)
+ .description("Test loan description").currencyCode("USD").digitsAfterDecimal(2).daysInYearType(1).daysInMonthType(1)
+ .interestRecalculationCompoundingMethod(0).recalculationRestFrequencyType(1).rescheduleStrategyMethod(1)
+ .recalculationRestFrequencyInterval(0).isInterestRecalculationEnabled(false).interestRateFrequencyType(2).locale("en_GB")
+ .numberOfRepayments(4).repaymentFrequencyType(2L).interestRatePerPeriod(2.0).repaymentEvery(1).minPrincipal(100.0)
+ .principal(1000.0).maxPrincipal(10000000.0).amortizationType(1).interestType(1).interestCalculationPeriodType(1)
+ .dateFormat("dd MMMM yyyy").transactionProcessingStrategyCode(DEFAULT_STRATEGY).accountingRule(3)
+ .fundSourceAccountId(fundSource.getAccountID().longValue())//
+ .loanPortfolioAccountId(loansReceivableAccount.getAccountID().longValue())//
+ .transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())//
+ .interestOnLoanAccountId(interestIncomeAccount.getAccountID().longValue())//
+ .incomeFromFeeAccountId(feeIncomeAccount.getAccountID().longValue())//
+ .incomeFromPenaltyAccountId(feeIncomeAccount.getAccountID().longValue())//
+ .incomeFromRecoveryAccountId(recoveriesAccount.getAccountID().longValue())//
+ .writeOffAccountId(writtenOffAccount.getAccountID().longValue())//
+ .overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())//
+ .receivableInterestAccountId(interestReceivableAccount.getAccountID().longValue())//
+ .receivableFeeAccountId(feeReceivableAccount.getAccountID().longValue())//
+ .receivablePenaltyAccountId(penaltyReceivableAccount.getAccountID().longValue())//
+ .goodwillCreditAccountId(goodwillExpenseAccount.getAccountID().longValue())//
+ .incomeFromGoodwillCreditInterestAccountId(goodwillIncomeAccount.getAccountID().longValue())//
+ .incomeFromGoodwillCreditFeesAccountId(goodwillIncomeAccount.getAccountID().longValue())//
+ .incomeFromGoodwillCreditPenaltyAccountId(goodwillIncomeAccount.getAccountID().longValue())//
+ .incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
+ .incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
+ .chargeOffExpenseAccountId(chargeOffExpenseAccount.getAccountID().longValue())//
+ .chargeOffFraudExpenseAccountId(chargeOffFraudExpenseAccount.getAccountID().longValue())//
+ .incomeFromChargeOffPenaltyAccountId(penaltyChargeOffAccount.getAccountID().longValue())//
+ ).getResourceId();
+ }
+
+ private static Long applyForLoanApplicationWithInterest(final Long clientID, final Long loanProductID, BigDecimal principal,
+ String applicationDisbursementDate) {
+ LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
+ final PostLoansRequest loanRequest = new PostLoansRequest() //
+ .loanTermFrequency(4).locale("en_GB").loanTermFrequencyType(2).numberOfRepayments(4).repaymentFrequencyType(2)
+ .interestRatePerPeriod(BigDecimal.valueOf(2)).repaymentEvery(1).principal(principal).amortizationType(1).interestType(1)
+ .interestCalculationPeriodType(1).dateFormat("dd MMMM yyyy").transactionProcessingStrategyCode(DEFAULT_STRATEGY)
+ .loanType("individual").expectedDisbursementDate(applicationDisbursementDate).submittedOnDate(applicationDisbursementDate)
+ .clientId(clientID).productId(loanProductID);
+ return loanTransactionHelper.applyLoan(loanRequest).getLoanId();
+ }
+
+ private static Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments,
+ boolean downPaymentEnabled, String downPaymentPercentage, boolean autoPayForDownPayment, LoanScheduleType loanScheduleType,
+ LoanScheduleProcessingType loanScheduleProcessingType, final Account... accounts) {
+ AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
+ AdvancedPaymentData goodwillCreditAllocation = createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT");
+ AdvancedPaymentData interestPaymentWaiver = createPaymentAllocation("INTEREST_PAYMENT_WAIVER", "LAST_INSTALLMENT");
+ AdvancedPaymentData merchantIssuedRefundAllocation = createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION");
+ AdvancedPaymentData payoutRefundAllocation = createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT");
+ LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
+ final String loanProductJSON = new LoanProductTestBuilder().withMinPrincipal(principal).withPrincipal(principal)
+ .withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments)
+ .withEnableDownPayment(downPaymentEnabled, downPaymentPercentage, autoPayForDownPayment).withinterestRatePerPeriod("0")
+ .withInterestRateFrequencyTypeAsMonths()
+ .withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
+ .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts)
+ .addAdvancedPaymentAllocation(defaultAllocation, goodwillCreditAllocation, merchantIssuedRefundAllocation,
+ payoutRefundAllocation, interestPaymentWaiver)
+ .withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withInterestTypeAsDecliningBalance().withMultiDisburse()
+ .withDisallowExpectedDisbursements(true).withLoanScheduleType(loanScheduleType)
+ .withLoanScheduleProcessingType(loanScheduleProcessingType).withDaysInMonth("30").withDaysInYear("365")
+ .withMoratorium("0", "0").build(null);
+ return loanTransactionHelper.getLoanProductId(loanProductJSON);
+ }
+
+ private static List<PaymentAllocationOrder> getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
+ AtomicInteger integer = new AtomicInteger(1);
+ return Arrays.stream(paymentAllocationTypes).map(pat -> {
+ PaymentAllocationOrder paymentAllocationOrder = new PaymentAllocationOrder();
+ paymentAllocationOrder.setPaymentAllocationRule(pat.name());
+ paymentAllocationOrder.setOrder(integer.getAndIncrement());
+ return paymentAllocationOrder;
+ }).collect(Collectors.toList());
+ }
+
+ private static AdvancedPaymentData createDefaultPaymentAllocation() {
+ AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
+ advancedPaymentData.setTransactionType("DEFAULT");
+ advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
+
+ List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
+ PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST,
+ PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
+ PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
+ PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
+
+ advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
+ return advancedPaymentData;
+ }
+
+ private static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule) {
+ AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
+ advancedPaymentData.setTransactionType(transactionType);
+ advancedPaymentData.setFutureInstallmentAllocationRule(futureInstallmentAllocationRule);
+
+ List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
+ PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST,
+ PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
+ PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
+ PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
+
+ advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
+ return advancedPaymentData;
+ }
+
+ private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final BigDecimal principal,
+ final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final BigDecimal interestRate,
+ final String expectedDisbursementDate, final String submittedOnDate, String transactionProcessorCode,
+ String loanScheduleProcessingType) {
+ LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
+ return loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(clientId).productId(loanProductId.longValue())
+ .expectedDisbursementDate(expectedDisbursementDate).dateFormat(DATETIME_PATTERN)
+ .transactionProcessingStrategyCode(transactionProcessorCode).locale("en").submittedOnDate(submittedOnDate)
+ .amortizationType(1).interestRatePerPeriod(interestRate).interestCalculationPeriodType(1).interestType(0)
+ .repaymentFrequencyType(0).repaymentEvery(repaymentAfterEvery).repaymentFrequencyType(0)
+ .numberOfRepayments(numberOfRepayments).loanTermFrequency(loanTermFrequency).loanTermFrequencyType(0).principal(principal)
+ .loanType("individual").loanScheduleProcessingType(loanScheduleProcessingType)
+ .maxOutstandingLoanBalance(BigDecimal.valueOf(35000)));
+ }
+
+ private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final BigDecimal principal,
+ final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final BigDecimal interestRate,
+ final String expectedDisbursementDate, final String submittedOnDate) {
+ return applyForLoanApplication(clientId, loanProductId, principal, loanTermFrequency, repaymentAfterEvery, numberOfRepayments,
+ interestRate, expectedDisbursementDate, submittedOnDate, LoanScheduleProcessingType.HORIZONTAL);
+ }
+
+ private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final BigDecimal principal,
+ final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final BigDecimal interestRate,
+ final String expectedDisbursementDate, final String submittedOnDate, LoanScheduleProcessingType loanScheduleProcessingType) {
+ LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
+ return applyForLoanApplication(clientId, loanProductId, principal, loanTermFrequency, repaymentAfterEvery, numberOfRepayments,
+ interestRate, expectedDisbursementDate, submittedOnDate,
+ AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY, loanScheduleProcessingType.name());
+ }
+
+ private static void validateLoanTransaction(GetLoansLoanIdResponse loanDetails, int index, double transactionAmount,
+ double principalPortion, double overPaidPortion, double loanBalance) {
+ assertEquals(transactionAmount, loanDetails.getTransactions().get(index).getAmount());
+ assertEquals(principalPortion, loanDetails.getTransactions().get(index).getPrincipalPortion());
+ assertEquals(overPaidPortion, loanDetails.getTransactions().get(index).getOverpaymentPortion());
+ assertEquals(loanBalance, loanDetails.getTransactions().get(index).getOutstandingLoanBalance());
+ }
+
+ private void validateLoanCharge(GetLoansLoanIdResponse loanDetails, int index, LocalDate dueDate, double charged, double paid,
+ double outstanding) {
+ GetLoansLoanIdLoanChargeData chargeData = loanDetails.getCharges().get(index);
+ assertEquals(dueDate, chargeData.getDueDate());
+ assertEquals(charged, chargeData.getAmount());
+ assertEquals(paid, chargeData.getAmountPaid());
+ assertEquals(outstanding, chargeData.getAmountOutstanding());
+ }
+
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java
index d744e74e6..80b50eaec 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BatchHelper.java
@@ -917,6 +917,23 @@ public final class BatchHelper {
return createTransactionRequest(requestId, reference, "goodwillCredit", amount, LocalDate.now(Utils.getZoneIdOfTenant()));
}
+ /**
+ * Creates and returns a {@link CreateTransactionLoanCommandStrategy} request with given request ID for payment
+ * waiver transaction.
+ *
+ *
+ * @param requestId
+ * the request ID
+ * @param reference
+ * the reference
+ * @param amount
+ * the amount
+ * @return BatchRequest the created {@link BatchRequest}
+ */
+ public static BatchRequest interestPaymentWaiverRequest(final Long requestId, final Long reference, final String amount) {
+ return createTransactionRequest(requestId, reference, "interestPaymentWaiver", amount, LocalDate.now(Utils.getZoneIdOfTenant()));
+ }
+
/**
* Creates and returns a {@link CreateTransactionLoanCommandStrategy} request with given request ID for merchant
* issued refund transaction.
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 434a2b8d5..4147b07b8 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
@@ -540,6 +540,16 @@ public class ExternalEventConfigurationHelper {
loanUndoReAmortizeBusinessEvent.put("enabled", false);
defaults.add(loanUndoReAmortizeBusinessEvent);
+ Map<String, Object> loanTransactionInterestPaymentWaiverPostBusinessEvent = new HashMap<>();
+ loanTransactionInterestPaymentWaiverPostBusinessEvent.put("type", "LoanTransactionInterestPaymentWaiverPostBusinessEvent");
+ loanTransactionInterestPaymentWaiverPostBusinessEvent.put("enabled", false);
+ defaults.add(loanTransactionInterestPaymentWaiverPostBusinessEvent);
+
+ Map<String, Object> loanTransactionInterestPaymentWaiverPreBusinessEvent = new HashMap<>();
+ loanTransactionInterestPaymentWaiverPreBusinessEvent.put("type", "LoanTransactionInterestPaymentWaiverPreBusinessEvent");
+ loanTransactionInterestPaymentWaiverPreBusinessEvent.put("enabled", false);
+ defaults.add(loanTransactionInterestPaymentWaiverPreBusinessEvent);
+
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 1c96f3a9e..534049b7b 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
@@ -601,6 +601,16 @@ public class LoanTransactionHelper extends IntegrationTest {
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "repayment"));
}
+ public PostLoansLoanIdTransactionsResponse makeInterestPaymentWaiver(final Long loanId,
+ final PostLoansLoanIdTransactionsRequest request) {
+ return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "interestPaymentWaiver"));
+ }
+
+ public PostLoansLoanIdTransactionsResponse makeInterestPaymentWaiver(final String loanExternalId,
+ final PostLoansLoanIdTransactionsRequest request) {
+ return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "interestPaymentWaiver"));
+ }
+
public PostLoansLoanIdTransactionsResponse reAge(final Long loanId, final PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "reAge"));
}