diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/AbstractPlatformException.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/AbstractPlatformException.java index 5b9a6fee5..11d10748d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/AbstractPlatformException.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exception/AbstractPlatformException.java @@ -66,6 +66,9 @@ public abstract class AbstractPlatformException extends RuntimeException { } private static Object[] filterThrowableCause(Object[] defaultUserMessageArgs) { + if (defaultUserMessageArgs == null) { + return NO_ARGS; + } List filteredDefaultUserMessageArgs = new ArrayList<>(defaultUserMessageArgs.length); for (Object defaultUserMessageArg : defaultUserMessageArgs) { if (!(defaultUserMessageArg instanceof Throwable)) { 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 a37e32e82..c1b6c32e7 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 @@ -25,6 +25,7 @@ import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import java.math.BigDecimal; @@ -47,6 +48,7 @@ import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.portfolio.account.data.AccountTransferData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; +import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; @@ -135,6 +137,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "fromTransaction") private Set loanTransactionRelations = new HashSet<>(); + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "loanTransaction") + private LoanReAgeParameter loanReAgeParameter; + protected LoanTransaction() {} public static LoanTransaction incomePosting(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount, @@ -292,6 +297,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { if (LoanTransactionType.CHARGE_PAYMENT.equals(loanTransaction.getTypeOf())) { newTransaction.getLoanChargesPaid().addAll(loanTransaction.getLoanChargesPaid()); } + if (LoanTransactionType.REAGE.equals(loanTransaction.getTypeOf())) { + newTransaction.setLoanReAgeParameter(loanTransaction.getLoanReAgeParameter().getCopy(newTransaction)); + } return newTransaction; } @@ -1000,6 +1008,14 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return DateUtils.isEqual(getTransactionDate(), date); } + public LoanReAgeParameter getLoanReAgeParameter() { + return loanReAgeParameter; + } + + public void setLoanReAgeParameter(LoanReAgeParameter loanReAgeParameter) { + this.loanReAgeParameter = loanReAgeParameter; + } + // TODO missing hashCode(), equals(Object obj), but probably OK as long as // this is never stored in a Collection. } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java index fcba7a095..7c79c6505 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java @@ -22,12 +22,15 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import java.time.LocalDate; import lombok.AllArgsConstructor; import lombok.Getter; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; @Entity @Table(name = "m_loan_reage_parameter") @@ -35,9 +38,9 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; @Getter public class LoanReAgeParameter extends AbstractAuditableWithUTCDateTimeCustom { - // intentionally not doing a JPA relationship since it's not necessary - @Column(name = "loan_transaction_id", nullable = false) - private Long loanTransactionId; + @OneToOne + @JoinColumn(name = "loan_transaction_id", nullable = false) + private LoanTransaction loanTransaction; @Enumerated(EnumType.STRING) @Column(name = "frequency_type", nullable = false) @@ -54,4 +57,8 @@ public class LoanReAgeParameter extends AbstractAuditableWithUTCDateTimeCustom { // for JPA, don't use protected LoanReAgeParameter() {} + + public LoanReAgeParameter getCopy(LoanTransaction loanTransaction) { + return new LoanReAgeParameter(loanTransaction, frequencyType, frequencyNumber, startDate, numberOfInstallments); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 10c6e1c41..37b920325 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -1293,12 +1293,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } private void handleReAge(LoanTransaction loanTransaction, TransactionCtx ctx) { + loanTransaction.resetDerivedComponents(); MonetaryCurrency currency = ctx.getCurrency(); List installments = ctx.getInstallments(); - // Either we have the transaction id or we need to fetch it from context - Long loanTransactionId = loanTransaction.getId() != null ? loanTransaction.getId() - : ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction); - LoanReAgeParameter reAgeParameter = reAgingParameterRepository.findByLoanTransactionId(loanTransactionId).orElseThrow(); + AtomicReference outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency)); installments.forEach(i -> { Money principalOutstanding = i.getPrincipalOutstanding(currency); @@ -1308,29 +1306,39 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } }); - Money calculatedPrincipal = outstandingPrincipalBalance.get().dividedBy(reAgeParameter.getNumberOfInstallments(), - MoneyHelper.getRoundingMode()); - Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf(); - if (installmentAmountInMultiplesOf != null) { - calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf); + loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(), Money.zero(currency), Money.zero(currency), + Money.zero(currency)); + + Money calculatedPrincipal = Money.zero(currency); + Money adjustCalculatedPrincipal = Money.zero(currency); + if (outstandingPrincipalBalance.get().isGreaterThanZero()) { + calculatedPrincipal = outstandingPrincipalBalance.get() + .dividedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(), MoneyHelper.getRoundingMode()); + Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf(); + if (installmentAmountInMultiplesOf != null) { + calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf); + } + adjustCalculatedPrincipal = outstandingPrincipalBalance.get() + .minus(calculatedPrincipal.multipliedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments())); } - Money adjustCalculatedPrincipal = outstandingPrincipalBalance.get() - .minus(calculatedPrincipal.multipliedBy(reAgeParameter.getNumberOfInstallments())); LoanRepaymentScheduleInstallment lastNormalInstallment = installments.stream().filter(i -> !i.isDownPayment()) .reduce((first, second) -> second).orElseThrow(); LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment( lastNormalInstallment.getLoan(), lastNormalInstallment.getInstallmentNumber() + 1, lastNormalInstallment.getDueDate(), - reAgeParameter.getStartDate(), calculatedPrincipal.getAmount()); + loanTransaction.getLoanReAgeParameter().getStartDate(), calculatedPrincipal.getAmount()); installments.add(reAgedInstallment); - for (int i = 1; i < reAgeParameter.getNumberOfInstallments(); i++) { - LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(reAgeParameter, reAgedInstallment.getDueDate()); + reAgedInstallment.updateDerivedFields(currency, loanTransaction.getTransactionDate()); + + for (int i = 1; i < loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(); i++) { + LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(), + reAgedInstallment.getDueDate()); reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(), reAgedInstallment.getInstallmentNumber() + 1, reAgedInstallment.getDueDate(), calculatedDueDate, calculatedPrincipal.getAmount()); installments.add(reAgedInstallment); + reAgedInstallment.updateDerivedFields(currency, loanTransaction.getTransactionDate()); } reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal); - reprocessInstallmentsOrder(installments); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionNotFoundException.java index 05ab128e0..58fb42c47 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionNotFoundException.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanTransactionNotFoundException.java @@ -28,6 +28,10 @@ import org.springframework.dao.EmptyResultDataAccessException; */ public class LoanTransactionNotFoundException extends AbstractPlatformResourceNotFoundException { + public LoanTransactionNotFoundException(final String msg) { + super("error.msg.loan.transaction.not.found", msg); + } + public LoanTransactionNotFoundException(final Long id) { super("error.msg.loan.transaction.id.invalid", "Transaction with identifier " + id + " does not exist", id); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java index 8b27424af..4db6b7e59 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java @@ -51,6 +51,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParamet import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository; 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.LoanTransactionNotFoundException; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; @@ -80,11 +81,10 @@ public class LoanReAgingServiceImpl { changes.put(LoanReAgingApiConstants.dateFormatParameterName, command.dateFormat()); LoanTransaction reAgeTransaction = createReAgeTransaction(loan, command); - // important to do a flush before creating the reage parameter since it needs the ID - loanTransactionRepository.saveAndFlush(reAgeTransaction); - LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command); - reAgingParameterRepository.saveAndFlush(reAgeParameter); + reAgeTransaction.setLoanReAgeParameter(reAgeParameter); + + loanTransactionRepository.saveAndFlush(reAgeTransaction); final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory .determineProcessor(loan.transactionProcessingStrategy()); @@ -119,7 +119,7 @@ public class LoanReAgingServiceImpl { LoanTransaction reAgeTransaction = findLatestNonReversedReAgeTransaction(loan); if (reAgeTransaction == null) { - // TODO: when validations implemented; throw exception if there isn't a reage transaction available + throw new LoanTransactionNotFoundException("Re-Age transaction for loan was not found"); } reverseReAgeTransaction(reAgeTransaction, command); loanTransactionRepository.saveAndFlush(reAgeTransaction); @@ -177,8 +177,7 @@ public class LoanReAgingServiceImpl { LocalDate startDate = command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate); Integer numberOfInstallments = command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments); Integer periodFrequencyNumber = command.integerValueOfParameterNamed(LoanReAgingApiConstants.frequencyNumber); - return new LoanReAgeParameter(reAgeTransaction.getId(), periodFrequencyType, periodFrequencyNumber, startDate, - numberOfInstallments); + return new LoanReAgeParameter(reAgeTransaction, periodFrequencyType, periodFrequencyNumber, startDate, numberOfInstallments); } private void reProcessLoanTransactions(Loan loan) { diff --git a/fineract-provider/src/main/resources/jpa/persistence.xml b/fineract-provider/src/main/resources/jpa/persistence.xml index bd8bd8000..bf3f727b5 100644 --- a/fineract-provider/src/main/resources/jpa/persistence.xml +++ b/fineract-provider/src/main/resources/jpa/persistence.xml @@ -118,6 +118,7 @@ org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTypeListConverter + org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter org.apache.fineract.interoperation.domain.InteropIdentifier org.apache.fineract.portfolio.interestratechart.domain.InterestIncentives 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 95156bc93..a16633f3d 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 @@ -90,6 +90,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.system.CodeHelper; import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper; import org.apache.fineract.integrationtests.useradministration.users.UserHelper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; 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; @@ -824,6 +825,12 @@ public abstract class BaseLoanIntegrationTest { assertEquals(actualMaturityDate, loanDetails.getTimeline().getActualMaturityDate()); } + protected void verifyLoanStatus(long loanId, LoanStatus loanStatus) { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + + assertEquals(loanStatus.getCode(), loanDetails.getStatus().getCode()); + } + @RequiredArgsConstructor public static class BatchRequestBuilder { @@ -964,6 +971,7 @@ public abstract class BaseLoanIntegrationTest { public static final Integer MONTHS = 2; public static final String MONTHS_STRING = "MONTHS"; + public static final Integer DAYS = 0; public static final String DAYS_STRING = "DAYS"; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java index 2db311f86..199c5702f 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java @@ -18,6 +18,10 @@ */ package org.apache.fineract.integrationtests.loan.reaging; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.math.BigDecimal; import java.time.LocalDate; import java.util.UUID; @@ -28,12 +32,15 @@ import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdResponse; 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.client.util.CallFailedRuntimeException; import org.apache.fineract.integrationtests.BaseLoanIntegrationTest; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.junit.jupiter.api.Test; public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest { @@ -406,4 +413,221 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest { checkMaturityDates(loanId, LocalDate.of(2023, 7, 12), LocalDate.of(2023, 7, 12)); }); } + + @Test + public void test_LoanReAgeReverseReplay_Works() { + AtomicLong createdLoanId = new AtomicLong(); + + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + int numberOfRepayments = 3; + int repaymentEvery = 15; + + // Create Loan Product + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() // + .numberOfRepayments(numberOfRepayments) // + .repaymentEvery(repaymentEvery) // + .installmentAmountInMultiplesOf(null) // + .enableDownPayment(true) // + .disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)) // + .enableAutoRepaymentForDownPayment(true) // + .repaymentFrequencyType(RepaymentFrequencyType.DAYS.longValue()); // + + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + Long loanProductId = loanProductResponse.getResourceId(); + + // Apply and Approve Loan + double amount = 1250.0; + + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)// + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)// + .repaymentEvery(repaymentEvery)// + .loanTermFrequency(numberOfRepayments * repaymentEvery)// + .repaymentFrequencyType(RepaymentFrequencyType.DAYS)// + .loanTermFrequencyType(RepaymentFrequencyType.DAYS); + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest); + + PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), + approveLoanRequest(amount, "01 January 2023")); + + Long loanId = approvedLoanResult.getLoanId(); + + // disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(500.0), "01 January 2023"); + + // verify transactions + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "01 January 2023"), // + transaction(125.0, "Down Payment", "01 January 2023") // + ); + + // verify schedule + verifyRepaymentSchedule(loanId, // + installment(500.0, null, "01 January 2023"), // + installment(125.0, true, "01 January 2023"), // + installment(125.0, false, "16 January 2023"), // + installment(125.0, false, "31 January 2023"), // + installment(125.0, false, "15 February 2023") // + ); + checkMaturityDates(loanId, LocalDate.of(2023, 2, 15), LocalDate.of(2023, 2, 15)); + createdLoanId.set(loanId); + }); + + runAt("27 February 2023", () -> { + + long loanId = createdLoanId.get(); + + // create re-age transaction + reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "01 March 2023", 6); + + // verify transactions + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "01 January 2023"), // + transaction(125.0, "Down Payment", "01 January 2023"), // + transaction(375.0, "Re-age", "27 February 2023") // + ); + + verifyRepaymentSchedule(loanId, // + installment(500.0, null, "01 January 2023"), // + installment(125.0, true, "01 January 2023"), // + installment(0.0, true, "16 January 2023"), // + installment(0.0, true, "31 January 2023"), // + installment(0.0, true, "15 February 2023"), // + installment(62.5, false, "01 March 2023"), // + installment(62.5, false, "01 April 2023"), // + installment(62.5, false, "01 May 2023"), // + installment(62.5, false, "01 June 2023"), // + installment(62.5, false, "01 July 2023"), // + installment(62.5, false, "01 August 2023") // + ); + checkMaturityDates(loanId, LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1)); + + loanTransactionHelper.makeLoanRepayment(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN) + .transactionDate("01 February 2023").locale("en").transactionAmount(125.0)); + + // verify transactions + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "01 January 2023"), // + transaction(125.0, "Down Payment", "01 January 2023"), // + transaction(125.0, "Repayment", "01 February 2023"), // + transaction(250.0, "Re-age", "27 February 2023") // + ); + + verifyRepaymentSchedule(loanId, // + installment(500.0, null, "01 January 2023"), // + installment(125.0, true, "01 January 2023"), // + installment(125.0, true, "16 January 2023"), // + installment(0.0, true, "31 January 2023"), // + installment(0.0, true, "15 February 2023"), // + installment(41.67, false, "01 March 2023"), // + installment(41.67, false, "01 April 2023"), // + installment(41.67, false, "01 May 2023"), // + installment(41.67, false, "01 June 2023"), // + installment(41.67, false, "01 July 2023"), // + installment(41.65, false, "01 August 2023") // + ); + checkMaturityDates(loanId, LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1)); + }); + runAt("28 February 2023", () -> { + + long loanId = createdLoanId.get(); + PostLoansLoanIdTransactionsResponse repaymentResponse = loanTransactionHelper.makeLoanRepayment(loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("02 February 2023").locale("en") + .transactionAmount(250.0)); + + // verify transactions + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "01 January 2023"), // + transaction(125.0, "Down Payment", "01 January 2023"), // + transaction(125.0, "Repayment", "01 February 2023"), // + transaction(250.0, "Repayment", "02 February 2023"), // + transaction(0.0, "Re-age", "27 February 2023") // + ); + + verifyRepaymentSchedule(loanId, // + installment(500.0, null, "01 January 2023"), // + installment(125.0, true, "01 January 2023"), // + installment(125.0, true, "16 January 2023"), // + installment(125.0, true, "31 January 2023"), // + installment(125.0, true, "15 February 2023"), // + installment(0.0, true, "01 March 2023"), // + installment(0.0, true, "01 April 2023"), // + installment(0.0, true, "01 May 2023"), // + installment(0.0, true, "01 June 2023"), // + installment(0.0, true, "01 July 2023"), // + installment(0.0, true, "01 August 2023") // + ); + checkMaturityDates(loanId, LocalDate.of(2023, 8, 1), LocalDate.of(2023, 2, 2)); + + verifyLoanStatus(loanId, LoanStatus.CLOSED_OBLIGATIONS_MET); + + loanTransactionHelper.reverseLoanTransaction(loanId, repaymentResponse.getResourceId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("28 February 2023") + .transactionAmount(0.0).locale("en")); + + // verify transactions + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "01 January 2023"), // + transaction(125.0, "Down Payment", "01 January 2023"), // + transaction(125.0, "Repayment", "01 February 2023"), // + reversedTransaction(250.0, "Repayment", "02 February 2023"), // + transaction(250.0, "Re-age", "27 February 2023") // + ); + + verifyRepaymentSchedule(loanId, // + installment(500.0, null, "01 January 2023"), // + installment(125.0, true, "01 January 2023"), // + installment(125.0, true, "16 January 2023"), // + installment(0.0, true, "31 January 2023"), // + installment(0.0, true, "15 February 2023"), // + installment(41.67, false, "01 March 2023"), // + installment(41.67, false, "01 April 2023"), // + installment(41.67, false, "01 May 2023"), // + installment(41.67, false, "01 June 2023"), // + installment(41.67, false, "01 July 2023"), // + installment(41.65, false, "01 August 2023") // + ); + + verifyLoanStatus(loanId, LoanStatus.ACTIVE); + checkMaturityDates(loanId, LocalDate.of(2023, 8, 1), LocalDate.of(2023, 8, 1)); + }); + + runAt("01 March 2023", () -> { + + long loanId = createdLoanId.get(); + // create re-age transaction + undoReAgeLoan(loanId); + + // verify transactions + verifyTransactions(loanId, // + transaction(500.0, "Disbursement", "01 January 2023"), // + transaction(125.0, "Down Payment", "01 January 2023"), // + transaction(125.0, "Repayment", "01 February 2023"), // + reversedTransaction(250.0, "Repayment", "02 February 2023"), // + reversedTransaction(250.0, "Re-age", "27 February 2023") // + ); + + verifyRepaymentSchedule(loanId, // + installment(500.0, null, "01 January 2023"), // + installment(125.0, true, "01 January 2023"), // + installment(125.0, true, "16 January 2023"), // + installment(125.0, false, "31 January 2023"), // + installment(125.0, false, "15 February 2023") // + ); + checkMaturityDates(loanId, LocalDate.of(2023, 2, 15), LocalDate.of(2023, 2, 15)); + }); + + runAt("02 March 2023", () -> { + + long loanId = createdLoanId.get(); + // create re-age transaction + CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper.undoReAge(loanId, new PostLoansLoanIdTransactionsRequest())); + assertEquals(404, exception.getResponse().code()); + assertTrue(exception.getMessage().contains("error.msg.loan.transaction.not.found")); + }); + } }