FINERACT-2418: Extend disbursal and reage events with changed terms flag

This commit is contained in:
Oleksii Novikov
2026-02-18 09:27:59 +00:00
committed by Adam Saghy
parent 136a966fc3
commit 1feb1d730f
17 changed files with 279 additions and 18 deletions
@@ -283,6 +283,15 @@
"items": "org.apache.fineract.avro.loan.v1.OriginatorDetailsV1"
}
]
},
{
"default": null,
"name": "flags",
"doc": "Optional flags associated with this transaction event",
"type": [
"null",
"org.apache.fineract.avro.loan.v1.LoanTransactionFlagsDataV1"
]
}
]
}
@@ -0,0 +1,15 @@
{
"name": "LoanTransactionFlagsDataV1",
"namespace": "org.apache.fineract.avro.loan.v1",
"doc": "Optional flags associated with a loan transaction event",
"type": "record",
"fields": [{
"default": null,
"name": "changedTerms",
"doc": "True if the transaction changed the number of loan repayment terms (installments excluding downpayment and additional)",
"type": [
"null",
"boolean"
]
}]
}
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.test.messaging.event.loan.transaction;
public class LoanReAgeTransactionEvent extends AbstractLoanTransactionEvent {
@Override
public String getEventName() {
return "LoanReAgeTransactionBusinessEvent";
}
}
@@ -37,6 +37,7 @@ import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.avro.loan.v1.LoanTransactionFlagsDataV1;
import org.apache.fineract.client.feign.FineractFeignClient;
import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
import org.apache.fineract.client.models.LoanScheduleData;
@@ -49,6 +50,7 @@ import org.apache.fineract.test.helper.ErrorMessageHelper;
import org.apache.fineract.test.helper.Utils;
import org.apache.fineract.test.messaging.EventAssertion;
import org.apache.fineract.test.messaging.event.loan.LoanReAgeEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanReAgeTransactionEvent;
import org.apache.fineract.test.stepdef.AbstractStepDef;
import org.apache.fineract.test.support.TestContextKey;
import org.junit.jupiter.api.Assertions;
@@ -414,4 +416,20 @@ public class LoanReAgingStepDef extends AbstractStepDef {
assertThat(exception.getDeveloperMessage()).contains(errorMessage);
}
@Then("LoanReAgeTransactionBusinessEvent has changedTerms {string}")
public void checkReAgeTransactionEventChangedTerms(final String expectedChangedTerms) {
final PostLoansLoanIdTransactionsResponse reAgingResponse = testContext().get(TestContextKey.LOAN_REAGING_RESPONSE);
Assertions.assertNotNull(reAgingResponse);
final Long transactionId = reAgingResponse.getResourceId();
Assertions.assertNotNull(transactionId);
final Boolean expectedValue = "null".equalsIgnoreCase(expectedChangedTerms) ? null : Boolean.valueOf(expectedChangedTerms);
eventAssertion.assertEvent(LoanReAgeTransactionEvent.class, transactionId).extractingData(loanTransactionDataV1 -> {
final LoanTransactionFlagsDataV1 flags = loanTransactionDataV1.getFlags();
final Boolean actualChangedTerms = flags == null ? null : flags.getChangedTerms();
assertThat(actualChangedTerms).as("changedTerms in LoanReAgeTransactionBusinessEvent").isEqualTo(expectedValue);
return null;
});
}
}
@@ -67,6 +67,7 @@ import org.apache.fineract.avro.loan.v1.LoanChargePaidByDataV1;
import org.apache.fineract.avro.loan.v1.LoanStatusEnumDataV1;
import org.apache.fineract.avro.loan.v1.LoanTransactionAdjustmentDataV1;
import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1;
import org.apache.fineract.avro.loan.v1.LoanTransactionFlagsDataV1;
import org.apache.fineract.client.feign.FineractFeignClient;
import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
import org.apache.fineract.client.models.AdvancedPaymentData;
@@ -162,6 +163,7 @@ import org.apache.fineract.test.messaging.event.loan.transaction.LoanCapitalized
import org.apache.fineract.test.messaging.event.loan.transaction.LoanChargeAdjustmentPostBusinessEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanChargeOffEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanChargeOffUndoEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanDisbursalTransactionEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionAccrualActivityPostEvent;
import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionContractTerminationPostBusinessEvent;
import org.apache.fineract.test.messaging.store.EventStore;
@@ -5922,4 +5924,35 @@ public class LoanStepDef extends AbstractStepDef {
return advancedPaymentData;
}
@When("Admin disburses the loan on {string} with {string} EUR transaction amount")
public void disburseLoanForEventVerification(String actualDisbursementDate, String transactionAmount) {
PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
assertNotNull(loanResponse);
long loanId = loanResponse.getLoanId();
PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest()
.actualDisbursementDate(actualDisbursementDate).transactionAmount(new BigDecimal(transactionAmount));
PostLoansLoanIdResponse loanDisburseResponse = ok(
() -> fineractClient.loans().stateTransitions(loanId, disburseRequest, Map.of("command", "disburse")));
testContext().set(TestContextKey.LOAN_DISBURSE_RESPONSE, loanDisburseResponse);
}
@Then("LoanDisbursalTransactionBusinessEvent has changedTerms {string}")
public void checkDisbursalTransactionEventChangedTerms(final String expectedChangedTerms) {
final PostLoansLoanIdResponse disburseResponse = testContext().get(TestContextKey.LOAN_DISBURSE_RESPONSE);
assertNotNull(disburseResponse);
final Long transactionId = disburseResponse.getSubResourceId();
assertNotNull(transactionId);
final Boolean expectedValue = "null".equalsIgnoreCase(expectedChangedTerms) ? null : Boolean.valueOf(expectedChangedTerms);
eventAssertion.assertEvent(LoanDisbursalTransactionEvent.class, transactionId).extractingData(loanTransactionDataV1 -> {
final LoanTransactionFlagsDataV1 flags = loanTransactionDataV1.getFlags();
final Boolean actualChangedTerms = flags == null ? null : flags.getChangedTerms();
assertThat(actualChangedTerms).as("changedTerms in LoanDisbursalTransactionBusinessEvent").isEqualTo(expectedValue);
return null;
});
}
}
@@ -9022,3 +9022,40 @@ Feature: Loan
| 01 February 2025 | Disbursement | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1500.0 | false | false |
| 01 February 2025 | Repayment | 1525.89 | 1500.0 | 25.89 | 0.0 | 0.0 | 0.0 | false | false |
| 01 February 2025 | Accrual | 25.89 | 0.0 | 25.89 | 0.0 | 0.0 | 0.0 | false | false |
Scenario: Verify that changedTerms is false in LoanDisbursalTransactionBusinessEvent for initial disbursement
When Admin sets the business date to "01 January 2024"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 1000 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 45 | DAYS | 15 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024"
When Admin disburses the loan on "01 January 2024" with "1000" EUR transaction amount
Then LoanDisbursalTransactionBusinessEvent has changedTerms "false"
Scenario: Verify that changedTerms is true in LoanDisbursalTransactionBusinessEvent when additional disbursement adds new terms
When Admin sets the business date to "01 January 2024"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_FULL_TERM_TRANCHE | 01 January 2024 | 200 | 9.4822 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "200" amount and expected disbursement date on "01 January 2024"
When Admin disburses the loan on "01 January 2024" with "100" EUR transaction amount
Then LoanDisbursalTransactionBusinessEvent has changedTerms "false"
When Admin sets the business date to "01 February 2024"
When Admin disburses the loan on "01 February 2024" with "100" EUR transaction amount
Then LoanDisbursalTransactionBusinessEvent has changedTerms "true"
Scenario: Verify that changedTerms is false in LoanDisbursalTransactionBusinessEvent when additional disbursement does not change terms
When Admin sets the business date to "01 January 2024"
When Admin creates a client with random data
When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE | 01 January 2024 | 300 | 9.4822 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "300" amount and expected disbursement date on "01 January 2024"
When Admin disburses the loan on "01 January 2024" with "100" EUR transaction amount
Then LoanDisbursalTransactionBusinessEvent has changedTerms "false"
When Admin sets the business date to "08 January 2024"
When Admin disburses the loan on "08 January 2024" with "200" EUR transaction amount
Then LoanDisbursalTransactionBusinessEvent has changedTerms "false"
@@ -46,7 +46,7 @@ Feature: LoanReAging
| 01 January 2024 | Down Payment | 250.0 | 250.0 | 0.0 | 0.0 | 0.0 | 750.0 | false |
| 20 February 2024 | Re-age | 750.0 | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | false |
Then Admin checks that delinquency range is: "NO_DELINQUENCY" and has delinquentDate ""
Then LoanReAgeTransactionBusinessEvent has changedTerms "true"
When Loan Pay-off is made on "20 February 2024"
Then Loan is closed with zero outstanding balance and it's all installments have obligations met
@@ -28,6 +28,10 @@ public class LoanDisbursalTransactionBusinessEvent extends LoanTransactionBusine
super(value);
}
public LoanDisbursalTransactionBusinessEvent(final LoanTransaction value, final LoanTransactionFlagsData flags) {
super(value, flags);
}
@Override
public String getType() {
return TYPE;
@@ -18,15 +18,24 @@
*/
package org.apache.fineract.infrastructure.event.business.domain.loan.transaction;
import lombok.Getter;
import org.apache.fineract.infrastructure.event.business.domain.AbstractBusinessEvent;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@Getter
public abstract class LoanTransactionBusinessEvent extends AbstractBusinessEvent<LoanTransaction> {
private static final String CATEGORY = "Loan";
private final LoanTransactionFlagsData flags;
public LoanTransactionBusinessEvent(LoanTransaction value) {
this(value, null);
}
public LoanTransactionBusinessEvent(LoanTransaction value, LoanTransactionFlagsData flags) {
super(value);
this.flags = flags;
}
@Override
@@ -0,0 +1,23 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.event.business.domain.loan.transaction;
public record LoanTransactionFlagsData(boolean changedTerms) {
}
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionFlagsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public class LoanReAgeTransactionBusinessEvent extends LoanTransactionBusinessEvent {
@@ -29,6 +30,10 @@ public class LoanReAgeTransactionBusinessEvent extends LoanTransactionBusinessEv
super(value);
}
public LoanReAgeTransactionBusinessEvent(LoanTransaction value, LoanTransactionFlagsData flags) {
super(value, flags);
}
@Override
public String getType() {
return TYPE;
@@ -1836,19 +1836,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return false;
}
public boolean hasChargeOffTransaction() {
return getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff);
}
public boolean hasAccelerateChargeOffStrategy() {
return LoanChargeOffBehaviour.ACCELERATE_MATURITY.equals(getLoanProductRelatedDetail().getChargeOffBehaviour());
}
public boolean hasContractTerminationTransaction() {
return getLoanTransactions().stream().anyMatch(t -> t.isContractTermination() && t.isNotReversed());
}
public boolean hasReAgingTransaction() {
return getLoanTransactions().stream().anyMatch(t -> t.isReAge() && t.isNotReversed());
public long getTermsCount() {
return getRepaymentScheduleInstallments().stream().filter(i -> !i.isDownPayment() && !i.isAdditional()).count();
}
}
@@ -33,6 +33,7 @@ public interface LoanTransactionDataMapper {
@Mapping(target = "customData", ignore = true)
@Mapping(target = "reversed", expression = "java(isReversed(source))")
@Mapping(target = "originators", ignore = true)
@Mapping(target = "flags", ignore = true)
LoanTransactionDataV1 map(LoanTransactionData source);
default boolean isReversed(LoanTransactionData source) {
@@ -23,8 +23,10 @@ import lombok.RequiredArgsConstructor;
import org.apache.avro.generic.GenericContainer;
import org.apache.fineract.avro.generator.ByteBufferSerializable;
import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1;
import org.apache.fineract.avro.loan.v1.LoanTransactionFlagsDataV1;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionFlagsData;
import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.loan.LoanTransactionDataMapper;
import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.AbstractBusinessEventWithCustomDataSerializer;
import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer;
@@ -60,6 +62,11 @@ public class LoanTransactionBusinessEventSerializer extends AbstractBusinessEven
final LoanTransactionDataV1 result = loanTransactionMapper.map(transactionData);
result.setCustomData(collectCustomData(event));
LoanTransactionFlagsData flags = event.getFlags();
if (flags != null) {
result.setFlags(LoanTransactionFlagsDataV1.newBuilder().setChangedTerms(flags.changedTerms()).build());
}
return result;
}
@@ -95,6 +95,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionFlagsData;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestRefundPostBusinessEvent;
@@ -368,6 +369,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
final long termsBefore = loan.getTermsCount();
if (canDisburse(loan)) {
// Get netDisbursalAmount from disbursal screen field.
final BigDecimal netDisbursalAmount = command
@@ -511,7 +514,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.filter(e -> LoanTransactionType.DISBURSEMENT.equals(e.getTypeOf())).findFirst().orElseThrow();
disbursalTransactionId = disbursalTransaction.getId();
disbursalTransactionExternalId = disbursalTransaction.getExternalId();
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction));
final long termsAfter = loan.getTermsCount();
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction,
new LoanTransactionFlagsData(termsAfter != termsBefore)));
}
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
@@ -757,17 +762,17 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
Money disburseAmount = loanDisbursementService.adjustDisburseAmount(loan, command, actualDisbursementDate);
boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal());
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
final long termsBefore = loan.getTermsCount();
LoanTransaction disbursementTransaction = null;
if (isAccountTransfer) {
disburseLoanToSavings(loan, command, disburseAmount, paymentDetail);
} else {
LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan, disburseAmount, paymentDetail,
actualDisbursementDate, txnExternalId, loan.getTotalOverpaidAsMoney());
disbursementTransaction = LoanTransaction.disbursement(loan, disburseAmount, paymentDetail, actualDisbursementDate,
txnExternalId, loan.getTotalOverpaidAsMoney());
disbursementTransaction.updateLoan(loan);
loan.addLoanTransaction(disbursementTransaction);
loanTransactionRepository.saveAndFlush(disbursementTransaction);
journalEntryPoster.postJournalEntriesForLoanTransaction(disbursementTransaction, false, false);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursementTransaction));
}
LocalDate recalculateFrom = null;
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
@@ -779,6 +784,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
disburseLoan(command, configurationDomainService.isPaymentTypeApplicableForDisbursementCharge(), paymentDetail, loan,
currentUser, changes, scheduleGeneratorDTO);
if (disbursementTransaction != null) {
final long termsAfter = loan.getTermsCount();
final LoanTransactionFlagsData additionalDisbursalFlags = new LoanTransactionFlagsData(termsAfter != termsBefore);
businessEventNotifierService.notifyPostBusinessEvent(
new LoanDisbursalTransactionBusinessEvent(disbursementTransaction, additionalDisbursalFlags));
}
loanAccrualsProcessingService.reprocessExistingAccruals(loan, true);
@@ -44,6 +44,7 @@ import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.reaging.LoanReAgeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.reaging.LoanUndoReAgeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionFlagsData;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanReAgeTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanUndoReAgeTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
@@ -105,6 +106,8 @@ public class LoanReAgingService {
reAgingValidator.validateReAge(loan, command);
BigDecimal userProvidedTxnAmount = command.bigDecimalValueOfParameterNamed(LoanReAgingApiConstants.transactionAmountParamName);
final long termsBefore = loan.getTermsCount();
final LoanTransaction reAgeTransaction = createReAgeTransaction(loan, command);
processReAgeTransaction(loan, reAgeTransaction, true);
validateUserProvidedTransactionAmount(userProvidedTxnAmount, reAgeTransaction);
@@ -116,9 +119,12 @@ public class LoanReAgingService {
changes.put(LoanReAgingApiConstants.dateFormatParameterName, command.dateFormat());
persistNote(loan, command, changes);
final long termsAfter = loan.getTermsCount();
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
businessEventNotifierService.notifyPostBusinessEvent(new LoanReAgeBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanReAgeTransactionBusinessEvent(reAgeTransaction));
businessEventNotifierService.notifyPostBusinessEvent(
new LoanReAgeTransactionBusinessEvent(reAgeTransaction, new LoanTransactionFlagsData(termsAfter != termsBefore)));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(reAgeTransaction.getId()) //
@@ -21,6 +21,8 @@ package org.apache.fineract.infrastructure.event.external.service.serialization.
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
@@ -32,7 +34,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionFlagsData;
import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.loan.LoanTransactionDataMapper;
import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.ExternalEventCustomDataSerializer;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
@@ -133,4 +137,62 @@ class LoanTransactionBusinessEventSerializerTest {
assertEquals("test_data_for_loan_transaction_2", new String(customData.get("test_key_2").array(), UTF_8));
}
@Test
void testTransactionFlagsSerializationWithChangedTerms() {
final long loanId = 1;
final long transactionId = 2;
final Loan loan = mock(Loan.class);
final LoanTransaction loanTransaction = mock(LoanTransaction.class);
final LoanTransactionData transactionData = mock(LoanTransactionData.class);
when(loan.getId()).thenReturn(loanId);
when(loanTransaction.getId()).thenReturn(transactionId);
when(loanTransaction.getLoan()).thenReturn(loan);
final LoanTransactionFlagsData flags = new LoanTransactionFlagsData(true);
final LoanDisbursalTransactionBusinessEvent event = new LoanDisbursalTransactionBusinessEvent(loanTransaction, flags);
when(loanReadPlatformService.retrieveLoanTransaction(loanId, transactionId)).thenReturn(transactionData);
when(loanChargePaidByReadService.fetchLoanChargesPaidByDataTransactionId(anyLong())).thenReturn(new ArrayList<>());
final LoanTransactionDataV1 expectedAvroData = LoanTransactionDataV1.newBuilder().setId(transactionId).setLoanId(loanId)
.setCustomData(new HashMap<>()).build();
when(loanTransactionMapper.map(any(LoanTransactionData.class))).thenReturn(expectedAvroData);
final LoanTransactionDataV1 result = (LoanTransactionDataV1) serializer.toAvroDTO(event);
assertNotNull(result);
assertNotNull(result.getFlags());
assertTrue(result.getFlags().getChangedTerms());
}
@Test
void testTransactionWithoutFlagsHasNullFlags() {
final long loanId = 1;
final long transactionId = 2;
final Loan loan = mock(Loan.class);
final LoanTransaction loanTransaction = mock(LoanTransaction.class);
final LoanTransactionData transactionData = mock(LoanTransactionData.class);
when(loan.getId()).thenReturn(loanId);
when(loanTransaction.getId()).thenReturn(transactionId);
when(loanTransaction.getLoan()).thenReturn(loan);
final LoanDisbursalTransactionBusinessEvent event = new LoanDisbursalTransactionBusinessEvent(loanTransaction);
when(loanReadPlatformService.retrieveLoanTransaction(loanId, transactionId)).thenReturn(transactionData);
when(loanChargePaidByReadService.fetchLoanChargesPaidByDataTransactionId(anyLong())).thenReturn(new ArrayList<>());
final LoanTransactionDataV1 expectedAvroData = LoanTransactionDataV1.newBuilder().setId(transactionId).setLoanId(loanId)
.setCustomData(new HashMap<>()).build();
when(loanTransactionMapper.map(any(LoanTransactionData.class))).thenReturn(expectedAvroData);
final LoanTransactionDataV1 result = (LoanTransactionDataV1) serializer.toAvroDTO(event);
assertNotNull(result);
assertNull(result.getFlags());
}
}