diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc index f02d6e090a6..cca1dab0c88 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc @@ -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" + ] } ] } diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionFlagsDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionFlagsDataV1.avsc new file mode 100644 index 00000000000..3fa5f7b3134 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionFlagsDataV1.avsc @@ -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" + ] + }] +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/loan/transaction/LoanReAgeTransactionEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/loan/transaction/LoanReAgeTransactionEvent.java new file mode 100644 index 00000000000..0dfa67a61f4 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/loan/transaction/LoanReAgeTransactionEvent.java @@ -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"; + } + +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java index 145c8e12afc..d70acbe9a30 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java @@ -37,6 +37,7 @@ 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.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 void checkCreateLoanReAgeFailure(DataTable table, String errorMessage) { 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; + }); + } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java index 23dcb365d3a..a8e8adbb2ec 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java @@ -67,6 +67,7 @@ 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.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; @@ -5942,4 +5944,35 @@ public static AdvancedPaymentData editPaymentAllocationFutureInstallment(String 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; + }); + } } diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature index 5ba518723a9..5223f7b12db 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature @@ -9021,4 +9021,41 @@ Feature: Loan | 01 January 2025 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false | | 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 | \ No newline at end of file + | 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" \ No newline at end of file diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature index 82e9f1ff17f..67622a61bb6 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature @@ -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 diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanDisbursalTransactionBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanDisbursalTransactionBusinessEvent.java index efdf3ac9bae..331c8d76278 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanDisbursalTransactionBusinessEvent.java +++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanDisbursalTransactionBusinessEvent.java @@ -28,6 +28,10 @@ public LoanDisbursalTransactionBusinessEvent(LoanTransaction value) { super(value); } + public LoanDisbursalTransactionBusinessEvent(LoanTransaction value, LoanTransactionFlagsData flags) { + super(value, flags); + } + @Override public String getType() { return TYPE; diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionBusinessEvent.java index 5a6b434b261..cd9846dd5b6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionBusinessEvent.java +++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionBusinessEvent.java @@ -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 { 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 diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionFlagsData.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionFlagsData.java new file mode 100644 index 00000000000..a8288b8e23f --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanTransactionFlagsData.java @@ -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) { + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reaging/LoanReAgeTransactionBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reaging/LoanReAgeTransactionBusinessEvent.java index fea932aa756..9095d533ba9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reaging/LoanReAgeTransactionBusinessEvent.java +++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/reaging/LoanReAgeTransactionBusinessEvent.java @@ -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 LoanReAgeTransactionBusinessEvent(LoanTransaction value) { super(value); } + public LoanReAgeTransactionBusinessEvent(LoanTransaction value, LoanTransactionFlagsData flags) { + super(value, flags); + } + @Override public String getType() { return TYPE; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanTransactionDataMapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanTransactionDataMapper.java index 3b0b906ea8f..f3f55d41930 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanTransactionDataMapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanTransactionDataMapper.java @@ -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) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanTransactionBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanTransactionBusinessEventSerializer.java index 6d2b303e2f2..e6c4ddd8e27 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanTransactionBusinessEventSerializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanTransactionBusinessEventSerializer.java @@ -23,8 +23,10 @@ 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 ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) { 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; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 49bc7e2a0fc..e035dccc373 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -95,6 +95,7 @@ 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,9 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); + final long termsBefore = loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isDownPayment() && !i.isAdditional()) + .count(); + if (canDisburse(loan)) { // Get netDisbursalAmount from disbursal screen field. final BigDecimal netDisbursalAmount = command @@ -511,7 +515,10 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand .filter(e -> LoanTransactionType.DISBURSEMENT.equals(e.getTypeOf())).findFirst().orElseThrow(); disbursalTransactionId = disbursalTransaction.getId(); disbursalTransactionExternalId = disbursalTransaction.getExternalId(); - businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction)); + final long termsAfter = loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isDownPayment() && !i.isAdditional()) + .count(); + businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction, + new LoanTransactionFlagsData(termsAfter != termsBefore))); } businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); @@ -757,17 +764,18 @@ public Map bulkLoanDisbursal(final JsonCommand command, final Co Money disburseAmount = loanDisbursementService.adjustDisburseAmount(loan, command, actualDisbursementDate); boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal()); final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); + final long additionalTermsBefore = loan.getRepaymentScheduleInstallments().stream() + .filter(i -> !i.isDownPayment() && !i.isAdditional()).count(); + 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 +787,14 @@ public Map bulkLoanDisbursal(final JsonCommand command, final Co } disburseLoan(command, configurationDomainService.isPaymentTypeApplicableForDisbursementCharge(), paymentDetail, loan, currentUser, changes, scheduleGeneratorDTO); + if (disbursementTransaction != null) { + final long additionalTermsAfter = loan.getRepaymentScheduleInstallments().stream() + .filter(i -> !i.isDownPayment() && !i.isAdditional()).count(); + final LoanTransactionFlagsData additionalDisbursalFlags = new LoanTransactionFlagsData( + additionalTermsAfter != additionalTermsBefore); + businessEventNotifierService.notifyPostBusinessEvent( + new LoanDisbursalTransactionBusinessEvent(disbursementTransaction, additionalDisbursalFlags)); + } loanAccrualsProcessingService.reprocessExistingAccruals(loan, true); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingService.java index 9ad702ea0de..4291f281514 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingService.java @@ -44,6 +44,7 @@ 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; @@ -118,7 +119,8 @@ public CommandProcessingResult reAge(final Long loanId, final JsonCommand comman // 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(true))); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(reAgeTransaction.getId()) // diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanTransactionBusinessEventSerializerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanTransactionBusinessEventSerializerTest.java index 0ea71f9cdb8..c593b3109cb 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanTransactionBusinessEventSerializerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanTransactionBusinessEventSerializerTest.java @@ -21,6 +21,8 @@ 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.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 @@ void testTransactionCustomDataSerialization() { 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()); + } + }