FINERACT-1883 - New loan repayment parameter

- [x] New loan repayment parameter
- [x] Integration test
This commit is contained in:
Janos Haber
2023-02-23 15:11:16 +01:00
committed by Arnold Gálovics
parent 670e0d55a3
commit 764f3de203
10 changed files with 72 additions and 30 deletions
@@ -66,6 +66,22 @@
"null", "null",
"bigdecimal" "bigdecimal"
] ]
},
{
"default": null,
"name": "lastRepaymentDate",
"type": [
"null",
"string"
]
},
{
"default": null,
"name": "lastRepaymentAmount",
"type": [
"null",
"bigdecimal"
]
} }
] ]
} }
@@ -112,10 +112,16 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
collectionData.setAvailableDisbursementAmount(loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount())); collectionData.setAvailableDisbursementAmount(loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()));
collectionData.setNextPaymentDueDate(loan.possibleNextRepaymentDate()); collectionData.setNextPaymentDueDate(loan.possibleNextRepaymentDate());
final LoanTransaction lastRepaymenTransaction = loan.getLastRepaymentTransaction(); final LoanTransaction lastPayment = loan.getLastPaymentTransaction();
if (lastRepaymenTransaction != null) { if (lastPayment != null) {
collectionData.setLastPaymentDate(lastRepaymenTransaction.getTransactionDate()); collectionData.setLastPaymentDate(lastPayment.getTransactionDate());
collectionData.setLastPaymentAmount(lastRepaymenTransaction.getAmount()); collectionData.setLastPaymentAmount(lastPayment.getAmount());
}
final LoanTransaction lastRepaymentTransaction = loan.getLastRepaymentTransaction();
if (lastRepaymentTransaction != null) {
collectionData.setLastRepaymentDate(lastRepaymentTransaction.getTransactionDate());
collectionData.setLastRepaymentAmount(lastRepaymentTransaction.getAmount());
} }
} }
@@ -979,6 +979,11 @@ final class LoansApiResourceSwagger {
public LocalDate lastPaymentDate; public LocalDate lastPaymentDate;
@Schema(example = "100.000000") @Schema(example = "100.000000")
public Double lastPaymentAmount; public Double lastPaymentAmount;
@Schema(example = "[2022, 07, 01]")
public LocalDate lastRepaymentDate;
@Schema(example = "100.000000")
public Double lastRepaymentAmount;
} }
@Schema(example = "1") @Schema(example = "1")
@@ -40,9 +40,12 @@ public final class CollectionData {
private LocalDate lastPaymentDate; private LocalDate lastPaymentDate;
private BigDecimal lastPaymentAmount; private BigDecimal lastPaymentAmount;
private LocalDate lastRepaymentDate;
private BigDecimal lastRepaymentAmount;
public static CollectionData template() { public static CollectionData template() {
final BigDecimal zero = BigDecimal.ZERO; 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);
} }
} }
@@ -3217,7 +3217,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
addLoanTransaction(loanTransaction); 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."; 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", throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.repayment.or.waiver.or.recovery.transaction",
errorMessage); errorMessage);
@@ -3604,7 +3604,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
final LocalDate currentTransactionDate = loanTransaction.getTransactionDate(); final LocalDate currentTransactionDate = loanTransaction.getTransactionDate();
for (final LoanTransaction previousTransaction : loanTransactions) { for (final LoanTransaction previousTransaction : loanTransactions) {
if (previousTransaction.isRepaymentType() && previousTransaction.isNotReversed() if (previousTransaction.isRepaymentLikeType() && previousTransaction.isNotReversed()
&& currentTransactionDate.isBefore(previousTransaction.getTransactionDate())) { && currentTransactionDate.isBefore(previousTransaction.getTransactionDate())) {
isAfterLatRepayment = false; isAfterLatRepayment = false;
break; break;
@@ -3642,7 +3642,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
LocalDate lastTransactionDate = null; LocalDate lastTransactionDate = null;
for (final LoanTransaction transaction : this.loanTransactions) { for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isRepaymentType() && transaction.isNonZero()) { if (transaction.isRepaymentLikeType() && transaction.isNonZero()) {
lastTransactionDate = transaction.getTransactionDate(); lastTransactionDate = transaction.getTransactionDate();
} }
} }
@@ -3714,7 +3714,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
transactionForAdjustment.getTransactionDate()); transactionForAdjustment.getTransactionDate());
if (transactionForAdjustment.isNotRepaymentType() && transactionForAdjustment.isNotWaiver() if (transactionForAdjustment.isNotRepaymentLikeType() && transactionForAdjustment.isNotWaiver()
&& transactionForAdjustment.isNotCreditBalanceRefund()) { && transactionForAdjustment.isNotCreditBalanceRefund()) {
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver or credit balance refund can be adjusted."; final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver or credit balance refund can be adjusted.";
throw new InvalidLoanTransactionTypeException("transaction", throw new InvalidLoanTransactionTypeException("transaction",
@@ -3734,7 +3734,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_ADJUST_TRANSACTION, this); loanLifecycleStateMachine.transition(LoanEvent.LOAN_ADJUST_TRANSACTION, this);
} }
if (newTransactionDetail.isRepaymentType() || newTransactionDetail.isInterestWaiver()) { if (newTransactionDetail.isRepaymentLikeType() || newTransactionDetail.isInterestWaiver()) {
changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine, changedTransactionDetail = handleRepaymentOrRecoveryOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine,
transactionForAdjustment, scheduleGeneratorDTO); transactionForAdjustment, scheduleGeneratorDTO);
} }
@@ -4227,7 +4227,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
Money cumulativePaid = Money.zero(loanCurrency()); Money cumulativePaid = Money.zero(loanCurrency());
for (final LoanTransaction repayment : this.loanTransactions) { for (final LoanTransaction repayment : this.loanTransactions) {
if (repayment.isRepaymentType() && !repayment.isReversed()) { if (repayment.isRepaymentLikeType() && !repayment.isReversed()) {
cumulativePaid = cumulativePaid.plus(repayment.getAmount(loanCurrency())); cumulativePaid = cumulativePaid.plus(repayment.getAmount(loanCurrency()));
} }
} }
@@ -4550,7 +4550,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
&& !transaction.getTransactionDate().isAfter(tillDate)) { && !transaction.getTransactionDate().isAfter(tillDate)) {
if (transaction.isAccrual()) { if (transaction.isAccrual()) {
receivableInterest = receivableInterest.plus(transaction.getInterestPortion(getCurrency())); receivableInterest = receivableInterest.plus(transaction.getInterestPortion(getCurrency()));
} else if (transaction.isRepaymentType() || transaction.isInterestWaiver()) { } else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver()) {
receivableInterest = receivableInterest.minus(transaction.getInterestPortion(getCurrency())); receivableInterest = receivableInterest.minus(transaction.getInterestPortion(getCurrency()));
} }
} }
@@ -4942,7 +4942,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final LoanTransaction repaymentTransaction, public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final LoanTransaction repaymentTransaction,
final String reversedOrCreated) { final String reversedOrCreated) {
if (repaymentTransaction.isRepaymentType() && !repaymentTransaction.isChargeRefund()) { if (repaymentTransaction.isRepaymentLikeType() && !repaymentTransaction.isChargeRefund()) {
for (LoanTransaction txn : this.getLoanTransactions()) { for (LoanTransaction txn : this.getLoanTransactions()) {
if (txn.isChargeRefund() && repaymentTransaction.getTransactionDate().isBefore(txn.getTransactionDate())) { if (txn.isChargeRefund() && repaymentTransaction.getTransactionDate().isBefore(txn.getTransactionDate())) {
final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists"; 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() { public LocalDate getLastRepaymentDate() {
LocalDate currentTransactionDate = getDisbursementDate(); LocalDate currentTransactionDate = getDisbursementDate();
for (final LoanTransaction previousTransaction : this.loanTransactions) { for (final LoanTransaction previousTransaction : this.loanTransactions) {
if (previousTransaction.isRepaymentType() && currentTransactionDate.isBefore(previousTransaction.getTransactionDate())) { if (previousTransaction.isRepaymentLikeType() && currentTransactionDate.isBefore(previousTransaction.getTransactionDate())) {
currentTransactionDate = previousTransaction.getTransactionDate(); currentTransactionDate = previousTransaction.getTransactionDate();
} }
} }
return currentTransactionDate; return currentTransactionDate;
} }
public LoanTransaction getLastPaymentTransaction() {
return loanTransactions.stream() //
.filter(loanTransaction -> !loanTransaction.isReversed()) //
.filter(LoanTransaction::isRepaymentLikeType) //
.reduce((first, second) -> second) //
.orElse(null);
}
public LoanTransaction getLastRepaymentTransaction() { public LoanTransaction getLastRepaymentTransaction() {
return loanTransactions.stream() // return loanTransactions.stream() //
.filter(loanTransaction -> !loanTransaction.isReversed()) // .filter(loanTransaction -> !loanTransaction.isReversed()) //
.filter(LoanTransaction::isRepaymentType) // .filter(LoanTransaction::isRepayment) //
.reduce((first, second) -> second) // .reduce((first, second) -> second) //
.orElse(null); .orElse(null);
} }
@@ -6300,7 +6308,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
LocalDate lastTransactionDate = null; LocalDate lastTransactionDate = null;
for (final LoanTransaction transaction : this.loanTransactions) { for (final LoanTransaction transaction : this.loanTransactions) {
if ((transaction.isRepaymentType() || transaction.isRefundForActiveLoan() || transaction.isCreditBalanceRefund()) if ((transaction.isRepaymentLikeType() || transaction.isRefundForActiveLoan() || transaction.isCreditBalanceRefund())
&& transaction.isNonZero() && transaction.isNotReversed()) { && transaction.isNonZero() && transaction.isNotReversed()) {
lastTransactionDate = transaction.getTransactionDate(); lastTransactionDate = transaction.getTransactionDate();
} }
@@ -6349,8 +6357,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
loanTransactions = retrieveListOfTransactionsExcludeAccruals(); loanTransactions = retrieveListOfTransactionsExcludeAccruals();
Collections.reverse(loanTransactions); Collections.reverse(loanTransactions);
for (final LoanTransaction previousTransaction : loanTransactions) { for (final LoanTransaction previousTransaction : loanTransactions) {
if (lastTransactionDate.isBefore(previousTransaction.getTransactionDate()) if (lastTransactionDate.isBefore(previousTransaction.getTransactionDate()) && (previousTransaction.isRepaymentLikeType()
&& (previousTransaction.isRepaymentType() || previousTransaction.isWaiver() || previousTransaction.isChargePayment())) { || previousTransaction.isWaiver() || previousTransaction.isChargePayment())) {
throw new UndoLastTrancheDisbursementException(previousTransaction.getId()); throw new UndoLastTrancheDisbursementException(previousTransaction.getId());
} }
if (previousTransaction.getId().compareTo(lastDisbursalTransaction.getId()) < 0) { if (previousTransaction.getId().compareTo(lastDisbursalTransaction.getId()) < 0) {
@@ -6673,7 +6681,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency)); receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency));
receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency)); receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency));
receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(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)); receivableInterest = receivableInterest.minus(transaction.getInterestPortion(currency));
receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency)); receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency));
receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency)); receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency));
@@ -562,7 +562,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
this.manuallyAdjustedOrReversed = true; this.manuallyAdjustedOrReversed = true;
} }
public boolean isRepaymentType() { public boolean isRepaymentLikeType() {
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()
|| isChargeAdjustment(); || isChargeAdjustment();
} }
@@ -591,8 +591,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
return LoanTransactionType.CHARGE_ADJUSTMENT.equals(getTypeOf()) && isNotReversed(); return LoanTransactionType.CHARGE_ADJUSTMENT.equals(getTypeOf()) && isNotReversed();
} }
public boolean isNotRepaymentType() { public boolean isNotRepaymentLikeType() {
return !isRepaymentType(); return !isRepaymentLikeType();
} }
public boolean isIncomePosting() { public boolean isIncomePosting() {
@@ -165,7 +165,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
Collections.sort(installments, byDate); Collections.sort(installments, byDate);
} }
if (loanTransaction.isRepaymentType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
// pass through for new transactions // pass through for new transactions
if (loanTransaction.getId() == null) { if (loanTransaction.getId() == null) {
handleTransaction(loanTransaction, currency, installments, charges); handleTransaction(loanTransaction, currency, installments, charges);
@@ -306,7 +306,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getAmount(currency)); totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getAmount(currency));
} else if (transaction.isCreditBalanceRefund() || transaction.isChargeback()) { } else if (transaction.isCreditBalanceRefund() || transaction.isChargeback()) {
totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getOverPaymentPortion(currency)); totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getOverPaymentPortion(currency));
} else if (transaction.isRepaymentType()) { } else if (transaction.isRepaymentLikeType()) {
totalPaidInRepayments = totalPaidInRepayments.plus(transaction.getAmount(currency)); totalPaidInRepayments = totalPaidInRepayments.plus(transaction.getAmount(currency));
} }
} }
@@ -428,7 +428,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, final Money chargeAmountToProcess, final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, final Money chargeAmountToProcess,
final boolean isFeeCharge) { final boolean isFeeCharge) {
// to. // to.
if (loanTransaction.isRepaymentType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
loanTransaction.resetDerivedComponents(); loanTransaction.resetDerivedComponents();
} }
Money transactionAmountUnprocessed = processTransaction(loanTransaction, currency, installments, chargeAmountToProcess); Money transactionAmountUnprocessed = processTransaction(loanTransaction, currency, installments, chargeAmountToProcess);
@@ -686,7 +686,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
Money unProcessed = Money.zero(currency); Money unProcessed = Money.zero(currency);
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) { for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
Money amountToProcess = null; Money amountToProcess = null;
if (loanTransaction.isRepaymentType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
loanTransaction.resetDerivedComponents(); loanTransaction.resetDerivedComponents();
} }
if (loanTransaction.isInterestWaiver()) { if (loanTransaction.isInterestWaiver()) {
@@ -1197,7 +1197,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust); LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust);
if (newTransactionDetail.isRepaymentType() && thereIsNewTransaction) { if (newTransactionDetail.isRepaymentLikeType() && thereIsNewTransaction) {
eventData.setNewTransactionDetail(newTransactionDetail); eventData.setNewTransactionDetail(newTransactionDetail);
} }
Long entityId = transactionToAdjust.getId(); Long entityId = transactionToAdjust.getId();
@@ -115,7 +115,8 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2); LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L, LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing); 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(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket); when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket);
@@ -224,7 +224,7 @@ public class LoanTransactionSummaryTest {
.transactionAmount(20.0)); .transactionAmount(20.0));
final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, 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)); .transactionAmount(100.0));
final PostLoansLoanIdTransactionsResponse repaymentTransaction_2 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, final PostLoansLoanIdTransactionsResponse repaymentTransaction_2 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,
@@ -234,8 +234,11 @@ public class LoanTransactionSummaryTest {
// Retrieve Loan with loanId // Retrieve Loan with loanId
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) 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(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, private GetLoanProductsProductIdResponse createLoanProduct(final LoanTransactionHelper loanTransactionHelper,