Skip to content

Commit 2dde798

Browse files
authored
Merge pull request #5738
FINERACT-2421: the loan originator information missing events
2 parents 09f4421 + 06391a5 commit 2dde798

File tree

17 files changed

+1176
-199
lines changed

17 files changed

+1176
-199
lines changed

.github/workflows/verify-liquibase-backward-compatibility.yml

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,20 @@ jobs:
3131
TZ: Asia/Kolkata
3232

3333
steps:
34-
- name: Checkout the base branch (`develop`)
34+
- name: Checkout base commit
3535
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3636
with:
3737
repository: ${{ github.event.pull_request.base.repo.full_name }}
38-
ref: ${{ github.event.pull_request.base.ref }}
38+
ref: ${{ github.event.pull_request.base.sha }}
3939
fetch-depth: 0
40+
path: baseline
41+
42+
- name: Checkout PR merge commit
43+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
44+
with:
45+
ref: ${{ github.sha }}
46+
fetch-depth: 0
47+
path: current
4048

4149
- name: Set up JDK 21
4250
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
@@ -52,11 +60,29 @@ jobs:
5260
done
5361
5462
- name: Init base schema
63+
working-directory: baseline
5564
run: |
5665
./gradlew --no-daemon createPGDB -PdbName=fineract_tenants
5766
./gradlew --no-daemon createPGDB -PdbName=$DB_NAME
5867
59-
- name: Start backend on base branch
68+
- name: Print checked out revisions
69+
run: |
70+
echo "Base branch ref: ${{ github.event.pull_request.base.ref }}"
71+
echo "Base branch event SHA:"
72+
echo "${{ github.event.pull_request.base.sha }}"
73+
echo "PR head SHA:"
74+
echo "${{ github.event.pull_request.head.sha }}"
75+
echo "GitHub merge SHA:"
76+
echo "${{ github.sha }}"
77+
echo "Baseline revision:"
78+
git -C baseline rev-parse HEAD
79+
git -C baseline log -1 --oneline
80+
echo "Merged PR revision:"
81+
git -C current rev-parse HEAD
82+
git -C current log -1 --oneline
83+
84+
- name: Start backend on base commit
85+
working-directory: baseline
6086
run: |
6187
./gradlew :fineract-provider:devRun --args="\
6288
--spring.datasource.hikari.driverClassName=org.postgresql.Driver \
@@ -79,19 +105,16 @@ jobs:
79105
80106
start_ts=$(date +%s)
81107
while true; do
82-
# If the process died, fail fast
83108
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
84109
echo "Backend process exited before Actuator became available."
85110
exit 1
86111
fi
87112
88-
# Check endpoint
89113
if curl -kfsS "$ACTUATOR_URL" >/dev/null 2>&1; then
90114
echo "Actuator is up."
91115
break
92116
fi
93117
94-
# Timeout
95118
now_ts=$(date +%s)
96119
if [ $((now_ts - start_ts)) -ge "$TIMEOUT_SECONDS" ]; then
97120
echo "Timed out waiting for Actuator."
@@ -101,19 +124,15 @@ jobs:
101124
sleep "$INTERVAL_SECONDS"
102125
done
103126
104-
- name: Stop backend
127+
- name: Stop baseline backend
128+
if: always()
129+
working-directory: baseline
105130
run: |
106131
kill $(cat backend.pid)
107132
sleep 10
108133
109-
- name: Checkout the PR branch
110-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
111-
with:
112-
repository: ${{ github.event.pull_request.head.repo.full_name }}
113-
ref: ${{ github.event.pull_request.head.sha }}
114-
fetch-depth: 0
115-
116-
- name: Start backend on PR branch
134+
- name: Start backend on merged PR commit
135+
working-directory: current
117136
run: |
118137
./gradlew :fineract-provider:devRun --args="\
119138
--spring.datasource.hikari.driverClassName=org.postgresql.Driver \
@@ -136,19 +155,16 @@ jobs:
136155
137156
start_ts=$(date +%s)
138157
while true; do
139-
# If the process died, fail fast
140158
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
141159
echo "Backend process exited before Actuator became available."
142160
exit 1
143161
fi
144162
145-
# Check endpoint
146163
if curl -kfsS "$ACTUATOR_URL" >/dev/null 2>&1; then
147164
echo "Actuator is up."
148165
break
149166
fi
150167
151-
# Timeout
152168
now_ts=$(date +%s)
153169
if [ $((now_ts - start_ts)) -ge "$TIMEOUT_SECONDS" ]; then
154170
echo "Timed out waiting for Actuator."
@@ -157,7 +173,10 @@ jobs:
157173
158174
sleep "$INTERVAL_SECONDS"
159175
done
160-
- name: Stop backend
176+
177+
- name: Stop PR backend
178+
if: always()
179+
working-directory: current
161180
run: |
162181
kill $(cat backend.pid)
163182
sleep 10

fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationService.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,6 @@ private void validateEventConfigurationForIndividualTenant(FineractPlatformTenan
7878
log.debug("Missing from eventConfigurations: {}", CollectionUtils.subtract(eventConfigurations, eventClasses));
7979
}
8080

81-
if (eventClasses.size() != eventConfigurations.size()) {
82-
throw new ExternalEventConfigurationNotFoundException();
83-
}
84-
8581
for (String eventTypeClass : eventClasses) {
8682
if (!eventConfigurations.contains(eventTypeClass)) {
8783
throw new ExternalEventConfigurationNotFoundException(eventTypeClass);

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanOriginationStepDef.java

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@
2727
import io.cucumber.java.en.Then;
2828
import io.cucumber.java.en.When;
2929
import java.math.BigDecimal;
30+
import java.time.format.DateTimeFormatter;
3031
import java.util.List;
3132
import java.util.Map;
3233
import java.util.UUID;
3334
import java.util.concurrent.TimeUnit;
3435
import lombok.extern.slf4j.Slf4j;
36+
import org.apache.fineract.avro.loan.v1.LoanTransactionAdjustmentDataV1;
37+
import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1;
3538
import org.apache.fineract.avro.loan.v1.OriginatorDetailsV1;
3639
import org.apache.fineract.client.feign.FineractFeignClient;
3740
import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
@@ -41,11 +44,14 @@
4144
import org.apache.fineract.client.models.GetLoanOriginatorsResponse;
4245
import org.apache.fineract.client.models.GetLoansLoanIdOriginatorData;
4346
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
47+
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
4448
import org.apache.fineract.client.models.PostClientsResponse;
4549
import org.apache.fineract.client.models.PostLoanOriginatorsRequest;
4650
import org.apache.fineract.client.models.PostLoanOriginatorsResponse;
4751
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
4852
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
53+
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
54+
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
4955
import org.apache.fineract.client.models.PostLoansOriginatorData;
5056
import org.apache.fineract.client.models.PostLoansRequest;
5157
import org.apache.fineract.client.models.PostLoansResponse;
@@ -56,6 +62,8 @@
5662
import org.apache.fineract.test.helper.ErrorMessageHelper;
5763
import org.apache.fineract.test.messaging.EventAssertion;
5864
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;
5967
import org.apache.fineract.test.messaging.store.EventStore;
6068
import org.apache.fineract.test.stepdef.AbstractStepDef;
6169
import org.apache.fineract.test.support.TestContextKey;
@@ -65,6 +73,10 @@
6573
public class LoanOriginationStepDef extends AbstractStepDef {
6674

6775
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";
6880

6981
@Autowired
7082
private FineractFeignClient fineractClient;
@@ -603,6 +615,135 @@ public void deleteOriginatorShouldFailWithStatus(int expectedStatus) {
603615
log.info("Deleting originator {} failed with expected status {}", originatorId, expectedStatus);
604616
}
605617

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+
606747
// --- Helper methods ---
607748

608749
private long getLoanId() {

0 commit comments

Comments
 (0)