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 { } } - private void updateLoanSummaryDerivedFields() { + public void updateLoanSummaryDerivedFields() { if (isNotDisbursed()) { this.summary.zeroFields(); this.totalOverpaid = null; @@ -2586,9 +2586,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } 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 dataValidationErrors = new ArrayList<>(); final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid"; @@ -2799,7 +2800,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { 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 { 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 { .orElse(null); } - private boolean isOverPaid() { + public boolean isOverPaid() { return calculateTotalOverpayment().isGreaterThanZero(); } @@ -3209,7 +3210,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return getTotalRecoveredPayments(); } - private MonetaryCurrency loanCurrency() { + public MonetaryCurrency loanCurrency() { return this.loanRepaymentScheduleDetail.getCurrency(); } @@ -3479,7 +3480,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return getStatus().isClosed() || isCancelled(); } - private boolean isClosedObligationsMet() { + public boolean isClosedObligationsMet() { return getStatus().isClosedObligationsMet(); } @@ -6024,7 +6025,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } } - 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 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 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 accountMapForCredit = new LinkedHashMap<>(); + + Map 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 creditEntry : accountMapForCredit.entrySet()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, + creditEntry.getValue(), isReversal, creditEntry.getKey()); + } + + // create debit entries + for (Map.Entry 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 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 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 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 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 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 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 allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); + ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( + loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(), + loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); + for (final Map.Entry 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 existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); + final List existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); + + final String noteText = command.stringValueOfParameterNamed("note"); + final Map 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 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 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 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 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 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 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 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 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 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 @@ + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 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 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 first = loanProduct.getPaymentAllocation().stream() + .filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst(); + Assertions.assertTrue(first.isPresent()); + Assertions.assertEquals(defaultAllocation, first.get()); + + Optional 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 first = loanProduct.getPaymentAllocation().stream() + .filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst(); + Assertions.assertTrue(first.isPresent()); + Assertions.assertEquals(defaultAllocation, first.get()); + + Optional 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 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 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 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 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 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 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 loanTransactionInterestPaymentWaiverPostBusinessEvent = new HashMap<>(); + loanTransactionInterestPaymentWaiverPostBusinessEvent.put("type", "LoanTransactionInterestPaymentWaiverPostBusinessEvent"); + loanTransactionInterestPaymentWaiverPostBusinessEvent.put("enabled", false); + defaults.add(loanTransactionInterestPaymentWaiverPostBusinessEvent); + + Map 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")); }