|
27 | 27 | import io.cucumber.java.en.Then; |
28 | 28 | import io.cucumber.java.en.When; |
29 | 29 | import java.math.BigDecimal; |
| 30 | +import java.time.format.DateTimeFormatter; |
30 | 31 | import java.util.List; |
31 | 32 | import java.util.Map; |
32 | 33 | import java.util.UUID; |
33 | 34 | import java.util.concurrent.TimeUnit; |
34 | 35 | import lombok.extern.slf4j.Slf4j; |
| 36 | +import org.apache.fineract.avro.loan.v1.LoanTransactionAdjustmentDataV1; |
| 37 | +import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1; |
35 | 38 | import org.apache.fineract.avro.loan.v1.OriginatorDetailsV1; |
36 | 39 | import org.apache.fineract.client.feign.FineractFeignClient; |
37 | 40 | import org.apache.fineract.client.feign.util.CallFailedRuntimeException; |
|
41 | 44 | import org.apache.fineract.client.models.GetLoanOriginatorsResponse; |
42 | 45 | import org.apache.fineract.client.models.GetLoansLoanIdOriginatorData; |
43 | 46 | import org.apache.fineract.client.models.GetLoansLoanIdResponse; |
| 47 | +import org.apache.fineract.client.models.GetLoansLoanIdTransactions; |
44 | 48 | import org.apache.fineract.client.models.PostClientsResponse; |
45 | 49 | import org.apache.fineract.client.models.PostLoanOriginatorsRequest; |
46 | 50 | import org.apache.fineract.client.models.PostLoanOriginatorsResponse; |
47 | 51 | import org.apache.fineract.client.models.PostLoansLoanIdRequest; |
48 | 52 | import org.apache.fineract.client.models.PostLoansLoanIdResponse; |
| 53 | +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; |
| 54 | +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; |
49 | 55 | import org.apache.fineract.client.models.PostLoansOriginatorData; |
50 | 56 | import org.apache.fineract.client.models.PostLoansRequest; |
51 | 57 | import org.apache.fineract.client.models.PostLoansResponse; |
|
56 | 62 | import org.apache.fineract.test.helper.ErrorMessageHelper; |
57 | 63 | import org.apache.fineract.test.messaging.EventAssertion; |
58 | 64 | import org.apache.fineract.test.messaging.event.loan.LoanApprovedEvent; |
| 65 | +import org.apache.fineract.test.messaging.event.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent; |
| 66 | +import org.apache.fineract.test.messaging.event.loan.transaction.LoanAdjustTransactionBusinessEvent; |
59 | 67 | import org.apache.fineract.test.messaging.store.EventStore; |
60 | 68 | import org.apache.fineract.test.stepdef.AbstractStepDef; |
61 | 69 | import org.apache.fineract.test.support.TestContextKey; |
|
65 | 73 | public class LoanOriginationStepDef extends AbstractStepDef { |
66 | 74 |
|
67 | 75 | private static final long NON_EXISTENT_ID = Long.MAX_VALUE; |
| 76 | + private static final String DATE_FORMAT = "dd MMMM yyyy"; |
| 77 | + private static final String DEFAULT_LOCALE = "en"; |
| 78 | + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT); |
| 79 | + private static final String ADJUSTED_TRANSACTION_ID = "adjustedTransactionId"; |
68 | 80 |
|
69 | 81 | @Autowired |
70 | 82 | private FineractFeignClient fineractClient; |
@@ -603,6 +615,135 @@ public void deleteOriginatorShouldFailWithStatus(int expectedStatus) { |
603 | 615 | log.info("Deleting originator {} failed with expected status {}", originatorId, expectedStatus); |
604 | 616 | } |
605 | 617 |
|
| 618 | + @When("Customer makes a repayment undo on {string} without event check") |
| 619 | + public void makeLoanRepaymentUndoWithoutEventCheck(String transactionDate) { |
| 620 | + eventStore.reset(); |
| 621 | + PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); |
| 622 | + long loanId = loanResponse.getLoanId(); |
| 623 | + PostLoansLoanIdTransactionsResponse repaymentResponse = testContext().get(TestContextKey.LOAN_REPAYMENT_RESPONSE); |
| 624 | + Long originalTransactionId = repaymentResponse.getResourceId(); |
| 625 | + |
| 626 | + PostLoansLoanIdTransactionsTransactionIdRequest repaymentUndoRequest = LoanRequestFactory.defaultRepaymentUndoRequest() |
| 627 | + .transactionDate(transactionDate).dateFormat(DATE_FORMAT).locale(DEFAULT_LOCALE); |
| 628 | + |
| 629 | + ok(() -> fineractClient.loanTransactions().adjustLoanTransaction(loanId, originalTransactionId, repaymentUndoRequest, |
| 630 | + Map.<String, Object>of())); |
| 631 | + testContext().set(ADJUSTED_TRANSACTION_ID, originalTransactionId); |
| 632 | + log.info("Repayment {} undo on loan {} (event check skipped for separate originator verification)", originalTransactionId, loanId); |
| 633 | + } |
| 634 | + |
| 635 | + @When("Customer adjusts the repayment on {string} to {double} EUR without event check") |
| 636 | + public void adjustLoanRepaymentWithoutEventCheck(String transactionDate, double transactionAmount) { |
| 637 | + eventStore.reset(); |
| 638 | + PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); |
| 639 | + long loanId = loanResponse.getLoanId(); |
| 640 | + PostLoansLoanIdTransactionsResponse repaymentResponse = testContext().get(TestContextKey.LOAN_REPAYMENT_RESPONSE); |
| 641 | + Long originalTransactionId = repaymentResponse.getResourceId(); |
| 642 | + |
| 643 | + PostLoansLoanIdTransactionsTransactionIdRequest repaymentAdjustRequest = LoanRequestFactory |
| 644 | + .defaultRepaymentAdjustRequest(transactionAmount).transactionDate(transactionDate).dateFormat(DATE_FORMAT) |
| 645 | + .locale(DEFAULT_LOCALE); |
| 646 | + |
| 647 | + PostLoansLoanIdTransactionsResponse repaymentAdjustmentResponse = ok(() -> fineractClient.loanTransactions() |
| 648 | + .adjustLoanTransaction(loanId, originalTransactionId, repaymentAdjustRequest, Map.<String, Object>of())); |
| 649 | + testContext().set(TestContextKey.LOAN_REPAYMENT_UNDO_RESPONSE, repaymentAdjustmentResponse); |
| 650 | + testContext().set(ADJUSTED_TRANSACTION_ID, originalTransactionId); |
| 651 | + log.info("Repayment {} adjusted to {} on loan {} (event check skipped for separate originator verification)", originalTransactionId, |
| 652 | + transactionAmount, loanId); |
| 653 | + } |
| 654 | + |
| 655 | + @When("Customer reverses the waiver transaction on {string}") |
| 656 | + public void reverseWaiverTransaction(String transactionDate) { |
| 657 | + eventStore.reset(); |
| 658 | + PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); |
| 659 | + long loanId = loanResponse.getLoanId(); |
| 660 | + |
| 661 | + GetLoansLoanIdResponse loanDetails = ok( |
| 662 | + () -> fineractClient.loans().retrieveLoan(loanId, Map.<String, Object>of("associations", "transactions"))); |
| 663 | + Long waiveTransactionId = loanDetails.getTransactions().stream() |
| 664 | + .filter(t -> "loanTransactionType.waiveCharges".equals(t.getType().getCode())).map(GetLoansLoanIdTransactions::getId) |
| 665 | + .findFirst().orElseThrow(() -> new IllegalStateException("Waiver transaction not found on loan " + loanId)); |
| 666 | + |
| 667 | + PostLoansLoanIdTransactionsTransactionIdRequest undoRequest = LoanRequestFactory.defaultRepaymentUndoRequest() |
| 668 | + .transactionDate(transactionDate).dateFormat(DATE_FORMAT).locale(DEFAULT_LOCALE); |
| 669 | + |
| 670 | + ok(() -> fineractClient.loanTransactions().adjustLoanTransaction(loanId, waiveTransactionId, undoRequest, |
| 671 | + Map.<String, Object>of())); |
| 672 | + testContext().set(ADJUSTED_TRANSACTION_ID, waiveTransactionId); |
| 673 | + log.info("Waiver transaction {} reversed on loan {} (for originator event verification)", waiveTransactionId, loanId); |
| 674 | + } |
| 675 | + |
| 676 | + // --- Originator event verification steps --- |
| 677 | + |
| 678 | + @Then("LoanAdjustTransactionBusinessEvent is created with originator details in {string}") |
| 679 | + public void verifyOriginatorInAdjustEvent(String nestedField) { |
| 680 | + long loanId = getLoanId(); |
| 681 | + Long adjustedTransactionId = testContext().get(ADJUSTED_TRANSACTION_ID); |
| 682 | + String expectedExternalId = testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID); |
| 683 | + |
| 684 | + eventAssertion.assertEvent(LoanAdjustTransactionBusinessEvent.class, adjustedTransactionId).extractingData(adjustmentData -> { |
| 685 | + LoanTransactionDataV1 nested = resolveAdjustmentField(adjustmentData, nestedField); |
| 686 | + assertThat(nested).as("Field '%s' in LoanAdjustTransactionBusinessEvent", nestedField).isNotNull(); |
| 687 | + |
| 688 | + List<OriginatorDetailsV1> originators = nested.getOriginators(); |
| 689 | + assertThat(originators).as("Originators in %s should not be null or empty", nestedField).isNotNull().isNotEmpty(); |
| 690 | + assertThat(originators.get(0).getExternalId()).as("Originator externalId in %s", nestedField).isEqualTo(expectedExternalId); |
| 691 | + assertThat(originators.get(0).getStatus()).as("Originator status in %s", nestedField).isEqualTo("ACTIVE"); |
| 692 | + return adjustmentData.getTransactionToAdjust().getId(); |
| 693 | + }).isEqualTo(adjustedTransactionId); |
| 694 | + log.info("Verified originator {} in LoanAdjustTransactionBusinessEvent.{} for loan {}", expectedExternalId, nestedField, loanId); |
| 695 | + } |
| 696 | + |
| 697 | + @Then("LoanAdjustTransactionBusinessEvent is created without originator details in {string}") |
| 698 | + public void verifyNoOriginatorInAdjustEvent(String nestedField) { |
| 699 | + Long adjustedTransactionId = testContext().get(ADJUSTED_TRANSACTION_ID); |
| 700 | + |
| 701 | + eventAssertion.assertEvent(LoanAdjustTransactionBusinessEvent.class, adjustedTransactionId).extractingData(adjustmentData -> { |
| 702 | + LoanTransactionDataV1 nested = resolveAdjustmentField(adjustmentData, nestedField); |
| 703 | + assertThat(nested).as("Field '%s' in LoanAdjustTransactionBusinessEvent", nestedField).isNotNull(); |
| 704 | + |
| 705 | + List<OriginatorDetailsV1> originators = nested.getOriginators(); |
| 706 | + assertThat(originators).as("Originators in %s should be null or empty", nestedField).isNullOrEmpty(); |
| 707 | + return adjustmentData.getTransactionToAdjust().getId(); |
| 708 | + }).isEqualTo(adjustedTransactionId); |
| 709 | + log.info("Verified no originators in LoanAdjustTransactionBusinessEvent.{}", nestedField); |
| 710 | + } |
| 711 | + |
| 712 | + @Then("LoanAccrualTransactionCreatedBusinessEvent is created with originator details on {string}") |
| 713 | + public void verifyOriginatorInAccrualEvent(String date) { |
| 714 | + long loanId = getLoanId(); |
| 715 | + String expectedExternalId = testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID); |
| 716 | + |
| 717 | + GetLoansLoanIdResponse loanDetails = ok(() -> fineractClient.loans().retrieveLoan(loanId, |
| 718 | + Map.of("staffInSelectedOfficeOnly", "false", "associations", "transactions"))); |
| 719 | + GetLoansLoanIdTransactions accrualTransaction = loanDetails.getTransactions().stream() |
| 720 | + .filter(t -> date.equals(FORMATTER.format(t.getDate())) && "Accrual".equals(t.getType().getValue())) |
| 721 | + .reduce((first, second) -> second) |
| 722 | + .orElseThrow(() -> new IllegalStateException(String.format("No Accrual transaction found on %s", date))); |
| 723 | + |
| 724 | + eventAssertion.assertEvent(LoanAccrualTransactionCreatedBusinessEvent.class, accrualTransaction.getId()) |
| 725 | + .extractingData(loanTransactionDataV1 -> { |
| 726 | + List<OriginatorDetailsV1> originators = loanTransactionDataV1.getOriginators(); |
| 727 | + assertThat(originators).as("Originators in LoanAccrualTransactionCreatedBusinessEvent should not be null or empty") |
| 728 | + .isNotNull().isNotEmpty(); |
| 729 | + assertThat(originators.get(0).getExternalId()).as("Originator externalId in LoanAccrualTransactionCreatedBusinessEvent") |
| 730 | + .isEqualTo(expectedExternalId); |
| 731 | + assertThat(originators.get(0).getStatus()).as("Originator status in LoanAccrualTransactionCreatedBusinessEvent") |
| 732 | + .isEqualTo("ACTIVE"); |
| 733 | + return loanTransactionDataV1.getId(); |
| 734 | + }).isEqualTo(accrualTransaction.getId()); |
| 735 | + log.info("Verified originator {} in LoanAccrualTransactionCreatedBusinessEvent on {} for loan {}", expectedExternalId, date, |
| 736 | + loanId); |
| 737 | + } |
| 738 | + |
| 739 | + private LoanTransactionDataV1 resolveAdjustmentField(LoanTransactionAdjustmentDataV1 data, String field) { |
| 740 | + return switch (field) { |
| 741 | + case "transactionToAdjust" -> data.getTransactionToAdjust(); |
| 742 | + case "newTransactionDetail" -> data.getNewTransactionDetail(); |
| 743 | + default -> throw new IllegalArgumentException("Unknown adjustment field: " + field); |
| 744 | + }; |
| 745 | + } |
| 746 | + |
606 | 747 | // --- Helper methods --- |
607 | 748 |
|
608 | 749 | private long getLoanId() { |
|
0 commit comments