From 764f3de2035c263ab4098c8a2dc81c8a14b36e39 Mon Sep 17 00:00:00 2001 From: Janos Haber Date: Tue, 21 Feb 2023 23:23:30 +0100 Subject: [PATCH] FINERACT-1883 - New loan repayment parameter - [x] New loan repayment parameter - [x] Integration test --- .../main/avro/loan/v1/CollectionDataV1.avsc | 16 +++++++++ .../DelinquencyReadPlatformServiceImpl.java | 14 +++++--- .../api/LoansApiResourceSwagger.java | 5 +++ .../loanaccount/data/CollectionData.java | 5 ++- .../portfolio/loanaccount/domain/Loan.java | 36 +++++++++++-------- .../loanaccount/domain/LoanTransaction.java | 6 ++-- ...RepaymentScheduleTransactionProcessor.java | 8 ++--- ...WritePlatformServiceJpaRepositoryImpl.java | 2 +- ...tePlatformServiceRangeChangeEventTest.java | 3 +- .../LoanTransactionSummaryTest.java | 7 ++-- 10 files changed, 72 insertions(+), 30 deletions(-) diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/CollectionDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/CollectionDataV1.avsc index 20bdfccdc..e424e78e5 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/CollectionDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/CollectionDataV1.avsc @@ -66,6 +66,22 @@ "null", "bigdecimal" ] + }, + { + "default": null, + "name": "lastRepaymentDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "lastRepaymentAmount", + "type": [ + "null", + "bigdecimal" + ] } ] } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java index 8655942d5..40d13a5b8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -112,10 +112,16 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo collectionData.setAvailableDisbursementAmount(loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount())); collectionData.setNextPaymentDueDate(loan.possibleNextRepaymentDate()); - final LoanTransaction lastRepaymenTransaction = loan.getLastRepaymentTransaction(); - if (lastRepaymenTransaction != null) { - collectionData.setLastPaymentDate(lastRepaymenTransaction.getTransactionDate()); - collectionData.setLastPaymentAmount(lastRepaymenTransaction.getAmount()); + final LoanTransaction lastPayment = loan.getLastPaymentTransaction(); + if (lastPayment != null) { + collectionData.setLastPaymentDate(lastPayment.getTransactionDate()); + collectionData.setLastPaymentAmount(lastPayment.getAmount()); + } + + final LoanTransaction lastRepaymentTransaction = loan.getLastRepaymentTransaction(); + if (lastRepaymentTransaction != null) { + collectionData.setLastRepaymentDate(lastRepaymentTransaction.getTransactionDate()); + collectionData.setLastRepaymentAmount(lastRepaymentTransaction.getAmount()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index ed750ab63..0f4815360 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -979,6 +979,11 @@ final class LoansApiResourceSwagger { public LocalDate lastPaymentDate; @Schema(example = "100.000000") public Double lastPaymentAmount; + + @Schema(example = "[2022, 07, 01]") + public LocalDate lastRepaymentDate; + @Schema(example = "100.000000") + public Double lastRepaymentAmount; } @Schema(example = "1") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java index abfe84705..75d5cad38 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java @@ -40,9 +40,12 @@ public final class CollectionData { private LocalDate lastPaymentDate; private BigDecimal lastPaymentAmount; + private LocalDate lastRepaymentDate; + private BigDecimal lastRepaymentAmount; + public static CollectionData template() { final BigDecimal zero = BigDecimal.ZERO; - return new CollectionData(zero, 0L, null, 0L, null, zero, null, zero); + return new CollectionData(zero, 0L, null, 0L, null, zero, null, zero, null, zero); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index e077c3aac..f65136457 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -3217,7 +3217,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { addLoanTransaction(loanTransaction); } - if (loanTransaction.isNotRepaymentType() && loanTransaction.isNotWaiver() && loanTransaction.isNotRecoveryRepayment()) { + if (loanTransaction.isNotRepaymentLikeType() && loanTransaction.isNotWaiver() && loanTransaction.isNotRecoveryRepayment()) { final String errorMessage = "A transaction of type repayment or recovery repayment or waiver was expected but not received."; throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.repayment.or.waiver.or.recovery.transaction", errorMessage); @@ -3604,7 +3604,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LocalDate currentTransactionDate = loanTransaction.getTransactionDate(); for (final LoanTransaction previousTransaction : loanTransactions) { - if (previousTransaction.isRepaymentType() && previousTransaction.isNotReversed() + if (previousTransaction.isRepaymentLikeType() && previousTransaction.isNotReversed() && currentTransactionDate.isBefore(previousTransaction.getTransactionDate())) { isAfterLatRepayment = false; break; @@ -3642,7 +3642,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { LocalDate lastTransactionDate = null; for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isRepaymentType() && transaction.isNonZero()) { + if (transaction.isRepaymentLikeType() && transaction.isNonZero()) { lastTransactionDate = transaction.getTransactionDate(); } } @@ -3714,7 +3714,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, transactionForAdjustment.getTransactionDate()); - if (transactionForAdjustment.isNotRepaymentType() && transactionForAdjustment.isNotWaiver() + if (transactionForAdjustment.isNotRepaymentLikeType() && transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()) { final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver or credit balance refund can be adjusted."; throw new InvalidLoanTransactionTypeException("transaction", @@ -3734,7 +3734,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { loanLifecycleStateMachine.transition(LoanEvent.LOAN_ADJUST_TRANSACTION, this); } - if (newTransactionDetail.isRepaymentType() || newTransactionDetail.isInterestWaiver()) { + if (newTransactionDetail.isRepaymentLikeType() || newTransactionDetail.isInterestWaiver()) { changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment, scheduleGeneratorDTO); } @@ -4227,7 +4227,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { Money cumulativePaid = Money.zero(loanCurrency()); for (final LoanTransaction repayment : this.loanTransactions) { - if (repayment.isRepaymentType() && !repayment.isReversed()) { + if (repayment.isRepaymentLikeType() && !repayment.isReversed()) { cumulativePaid = cumulativePaid.plus(repayment.getAmount(loanCurrency())); } } @@ -4550,7 +4550,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { && !transaction.getTransactionDate().isAfter(tillDate)) { if (transaction.isAccrual()) { receivableInterest = receivableInterest.plus(transaction.getInterestPortion(getCurrency())); - } else if (transaction.isRepaymentType() || transaction.isInterestWaiver()) { + } else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver()) { receivableInterest = receivableInterest.minus(transaction.getInterestPortion(getCurrency())); } } @@ -4942,7 +4942,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final LoanTransaction repaymentTransaction, final String reversedOrCreated) { - if (repaymentTransaction.isRepaymentType() && !repaymentTransaction.isChargeRefund()) { + if (repaymentTransaction.isRepaymentLikeType() && !repaymentTransaction.isChargeRefund()) { for (LoanTransaction txn : this.getLoanTransactions()) { if (txn.isChargeRefund() && repaymentTransaction.getTransactionDate().isBefore(txn.getTransactionDate())) { final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists"; @@ -4968,17 +4968,25 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public LocalDate getLastRepaymentDate() { LocalDate currentTransactionDate = getDisbursementDate(); for (final LoanTransaction previousTransaction : this.loanTransactions) { - if (previousTransaction.isRepaymentType() && currentTransactionDate.isBefore(previousTransaction.getTransactionDate())) { + if (previousTransaction.isRepaymentLikeType() && currentTransactionDate.isBefore(previousTransaction.getTransactionDate())) { currentTransactionDate = previousTransaction.getTransactionDate(); } } return currentTransactionDate; } + public LoanTransaction getLastPaymentTransaction() { + return loanTransactions.stream() // + .filter(loanTransaction -> !loanTransaction.isReversed()) // + .filter(LoanTransaction::isRepaymentLikeType) // + .reduce((first, second) -> second) // + .orElse(null); + } + public LoanTransaction getLastRepaymentTransaction() { return loanTransactions.stream() // .filter(loanTransaction -> !loanTransaction.isReversed()) // - .filter(LoanTransaction::isRepaymentType) // + .filter(LoanTransaction::isRepayment) // .reduce((first, second) -> second) // .orElse(null); } @@ -6300,7 +6308,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { LocalDate lastTransactionDate = null; for (final LoanTransaction transaction : this.loanTransactions) { - if ((transaction.isRepaymentType() || transaction.isRefundForActiveLoan() || transaction.isCreditBalanceRefund()) + if ((transaction.isRepaymentLikeType() || transaction.isRefundForActiveLoan() || transaction.isCreditBalanceRefund()) && transaction.isNonZero() && transaction.isNotReversed()) { lastTransactionDate = transaction.getTransactionDate(); } @@ -6349,8 +6357,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { loanTransactions = retrieveListOfTransactionsExcludeAccruals(); Collections.reverse(loanTransactions); for (final LoanTransaction previousTransaction : loanTransactions) { - if (lastTransactionDate.isBefore(previousTransaction.getTransactionDate()) - && (previousTransaction.isRepaymentType() || previousTransaction.isWaiver() || previousTransaction.isChargePayment())) { + if (lastTransactionDate.isBefore(previousTransaction.getTransactionDate()) && (previousTransaction.isRepaymentLikeType() + || previousTransaction.isWaiver() || previousTransaction.isChargePayment())) { throw new UndoLastTrancheDisbursementException(previousTransaction.getId()); } if (previousTransaction.getId().compareTo(lastDisbursalTransaction.getId()) < 0) { @@ -6673,7 +6681,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency)); receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency)); receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(currency)); - } else if (transaction.isRepaymentType() || transaction.isChargePayment()) { + } else if (transaction.isRepaymentLikeType() || transaction.isChargePayment()) { receivableInterest = receivableInterest.minus(transaction.getInterestPortion(currency)); receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency)); receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency)); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 3faf4c9ee..e0bbdaad4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -562,7 +562,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { this.manuallyAdjustedOrReversed = true; } - public boolean isRepaymentType() { + public boolean isRepaymentLikeType() { return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() || isChargeAdjustment(); } @@ -591,8 +591,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return LoanTransactionType.CHARGE_ADJUSTMENT.equals(getTypeOf()) && isNotReversed(); } - public boolean isNotRepaymentType() { - return !isRepaymentType(); + public boolean isNotRepaymentLikeType() { + return !isRepaymentLikeType(); } public boolean isIncomePosting() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 76a95abbc..6ee1af846 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -165,7 +165,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen Collections.sort(installments, byDate); } - if (loanTransaction.isRepaymentType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { + if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { // pass through for new transactions if (loanTransaction.getId() == null) { handleTransaction(loanTransaction, currency, installments, charges); @@ -306,7 +306,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getAmount(currency)); } else if (transaction.isCreditBalanceRefund() || transaction.isChargeback()) { totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getOverPaymentPortion(currency)); - } else if (transaction.isRepaymentType()) { + } else if (transaction.isRepaymentLikeType()) { totalPaidInRepayments = totalPaidInRepayments.plus(transaction.getAmount(currency)); } } @@ -428,7 +428,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen final List installments, final Set charges, final Money chargeAmountToProcess, final boolean isFeeCharge) { // to. - if (loanTransaction.isRepaymentType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { + if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { loanTransaction.resetDerivedComponents(); } Money transactionAmountUnprocessed = processTransaction(loanTransaction, currency, installments, chargeAmountToProcess); @@ -686,7 +686,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen Money unProcessed = Money.zero(currency); for (final LoanTransaction loanTransaction : transactionsPostDisbursement) { Money amountToProcess = null; - if (loanTransaction.isRepaymentType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { + if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { loanTransaction.resetDerivedComponents(); } if (loanTransaction.isInterestWaiver()) { 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 839b826b8..96b1352cb 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 @@ -1197,7 +1197,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust); - if (newTransactionDetail.isRepaymentType() && thereIsNewTransaction) { + if (newTransactionDetail.isRepaymentLikeType() && thereIsNewTransaction) { eventData.setNewTransactionDetail(newTransactionDetail); } Long entityId = transactionToAdjust.getId(); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java index f52ed68cf..93d648dcb 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java @@ -115,7 +115,8 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2); LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L, loanForProcessing); - CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null); + CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null, + null, null); when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionSummaryTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionSummaryTest.java index a15bfa934..f03ad1954 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionSummaryTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionSummaryTest.java @@ -224,7 +224,7 @@ public class LoanTransactionSummaryTest { .transactionAmount(20.0)); final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, - new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("8 September 2022").locale("en") + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("7 September 2022").locale("en") .transactionAmount(100.0)); final PostLoansLoanIdTransactionsResponse repaymentTransaction_2 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, @@ -234,8 +234,11 @@ public class LoanTransactionSummaryTest { // Retrieve Loan with loanId GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanId); - assertEquals(100.0, loanDetails.getDelinquent().getLastPaymentAmount()); + assertEquals(20.0, loanDetails.getDelinquent().getLastPaymentAmount()); assertEquals(LocalDate.of(2022, 9, 8), loanDetails.getDelinquent().getLastPaymentDate()); + + assertEquals(100.0, loanDetails.getDelinquent().getLastRepaymentAmount()); + assertEquals(LocalDate.of(2022, 9, 7), loanDetails.getDelinquent().getLastRepaymentDate()); } private GetLoanProductsProductIdResponse createLoanProduct(final LoanTransactionHelper loanTransactionHelper,