From d80aa77c02c58d56f1fd5f4ec662716ffec2b750 Mon Sep 17 00:00:00 2001 From: Ruchi Dhamankar Date: Thu, 15 Dec 2022 21:04:15 +0530 Subject: [PATCH] FINERACT-1734-LoanRepaymentDue-Event-Enhancements-1 --- .../avro/loan/v1/LoanRepaymentDueDataV1.avsc | 42 ++++----------- .../main/avro/loan/v1/RepaymentDueDataV1.avsc | 8 +++ .../avro/loan/v1/RepaymentPastDueDataV1.avsc | 47 ++++++++++++++++ ...CheckLoanRepaymentOverdueBusinessStep.java | 4 +- .../LoanRepaymentBusinessEventSerializer.java | 15 ++++-- ...kLoanRepaymentOverdueBusinessStepTest.java | 12 ++--- ...nRepaymentBusinessEventSerializerTest.java | 53 +++++++++++++++++-- 7 files changed, 134 insertions(+), 47 deletions(-) create mode 100644 fineract-avro-schemas/src/main/avro/loan/v1/RepaymentPastDueDataV1.avsc diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanRepaymentDueDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanRepaymentDueDataV1.avsc index 50c05f577..87efc7499 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanRepaymentDueDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanRepaymentDueDataV1.avsc @@ -4,32 +4,16 @@ "type": "record", "fields": [ { - "default": null, - "name": "id", - "type": [ - "null", - "long" - ] + "name": "loanId", + "type": ["long"] + }, + { + "name": "loanAccountNo", + "type": ["string"] }, { "default": null, - "name": "accountNo", - "type": [ - "null", - "string" - ] - }, - { - "default": null, - "name": "externalId", - "type": [ - "null", - "string" - ] - }, - { - "default": null, - "name": "dueDate", + "name": "loanExternalId", "type": [ "null", "string" @@ -44,19 +28,15 @@ ] }, { - "default": null, - "name": "totalLoanAmountDue", - "type": [ - "null", - "bigdecimal" - ] + "name": "installment", + "type": ["org.apache.fineract.avro.loan.v1.RepaymentDueDataV1"] }, { "default": null, - "name": "repaymentDue", + "name": "pastDueAmount", "type": [ "null", - "org.apache.fineract.avro.loan.v1.RepaymentDueDataV1" + "org.apache.fineract.avro.loan.v1.RepaymentPastDueDataV1" ] } ] diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/RepaymentDueDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/RepaymentDueDataV1.avsc index d33076511..6a95e6efd 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/RepaymentDueDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/RepaymentDueDataV1.avsc @@ -11,6 +11,14 @@ "int" ] }, + { + "default": null, + "name": "installmentDueDate", + "type": [ + "null", + "string" + ] + }, { "default": null, "name": "principalAmountDue", diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/RepaymentPastDueDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/RepaymentPastDueDataV1.avsc new file mode 100644 index 000000000..41dc1f5f5 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/loan/v1/RepaymentPastDueDataV1.avsc @@ -0,0 +1,47 @@ +{ + "name": "RepaymentPastDueDataV1", + "namespace": "org.apache.fineract.avro.loan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "totalAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "principalAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "interestAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "feeAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "penaltyAmount", + "type": [ + "null", + "bigdecimal" + ] + } + ] +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java index 531720bfb..9c46f4e2d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java @@ -23,7 +23,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.event.business.domain.loan.repayment.LoanRepaymentOverdueBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.repayment.LoanRepaymentDueBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; @@ -45,7 +45,7 @@ public class CheckLoanRepaymentOverdueBusinessStep implements LoanCOBBusinessSte if (!repaymentSchedule.isObligationsMet()) { LocalDate installmentDueDate = repaymentSchedule.getDueDate(); if (installmentDueDate.plusDays(numberOfDaysAfterDueDateToRaiseEvent).equals(currentDate)) { - businessEventNotifierService.notifyPostBusinessEvent(new LoanRepaymentOverdueBusinessEvent(repaymentSchedule)); + businessEventNotifierService.notifyPostBusinessEvent(new LoanRepaymentDueBusinessEvent(repaymentSchedule)); break; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializer.java index 6e04102fe..d196e609c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializer.java @@ -25,6 +25,7 @@ import org.apache.fineract.avro.generator.ByteBufferSerializable; import org.apache.fineract.avro.generic.v1.CurrencyDataV1; import org.apache.fineract.avro.loan.v1.LoanRepaymentDueDataV1; import org.apache.fineract.avro.loan.v1.RepaymentDueDataV1; +import org.apache.fineract.avro.loan.v1.RepaymentPastDueDataV1; import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.repayment.LoanRepaymentBusinessEvent; import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.AvroDateTimeMapper; @@ -46,25 +47,31 @@ public class LoanRepaymentBusinessEventSerializer extends AbstractBusinessEventS LoanRepaymentBusinessEvent event = (LoanRepaymentBusinessEvent) rawEvent; LoanRepaymentScheduleInstallment repaymentInstallment = event.get(); Loan loan = repaymentInstallment.getLoan(); + Long id = loan.getId(); String accountNo = loan.getAccountNumber(); String externalId = loan.getExternalId().getValue(); - String dueDate = dataTimeMapper.mapLocalDate(repaymentInstallment.getDueDate()); MonetaryCurrency loanCurrency = loan.getCurrency(); CurrencyDataV1 currency = CurrencyDataV1.newBuilder().setCode(loanCurrency.getCode()) .setDecimalPlaces(loanCurrency.getDigitsAfterDecimal()).setInMultiplesOf(loanCurrency.getCurrencyInMultiplesOf()).build(); - BigDecimal totalLoanAmountDue = loan.getLoanSummary().getTotalOutstanding(); Integer installmentNumber = repaymentInstallment.getInstallmentNumber(); + String dueDate = dataTimeMapper.mapLocalDate(repaymentInstallment.getDueDate()); BigDecimal principalAmountDue = repaymentInstallment.getPrincipalOutstanding(loanCurrency).getAmount(); BigDecimal interestAmountDue = repaymentInstallment.getInterestOutstanding(loanCurrency).getAmount(); BigDecimal feeChargeAmountDue = repaymentInstallment.getFeeChargesOutstanding(loanCurrency).getAmount(); BigDecimal penaltyChargeAmountDue = repaymentInstallment.getPenaltyChargesOutstanding(loanCurrency).getAmount(); BigDecimal totalAmountDue = repaymentInstallment.getTotalOutstanding(loanCurrency).getAmount(); - RepaymentDueDataV1 repaymentDue = new RepaymentDueDataV1(installmentNumber, principalAmountDue, interestAmountDue, + RepaymentDueDataV1 repaymentDue = new RepaymentDueDataV1(installmentNumber, dueDate, principalAmountDue, interestAmountDue, feeChargeAmountDue, penaltyChargeAmountDue, totalAmountDue); - return new LoanRepaymentDueDataV1(id, accountNo, externalId, dueDate, currency, totalLoanAmountDue, repaymentDue); + + RepaymentPastDueDataV1 pastDue = new RepaymentPastDueDataV1(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, + BigDecimal.ZERO); + + LoanRepaymentDueDataV1 loanRepaymentDueDataV1 = LoanRepaymentDueDataV1.newBuilder().setLoanId(id).setLoanAccountNo(accountNo) + .setLoanExternalId(externalId).setCurrency(currency).setInstallment(repaymentDue).setPastDueAmount(pastDue).build(); + return loanRepaymentDueDataV1; } @Override diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java index 7e9c38dab..46e2e7602 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStepTest.java @@ -37,7 +37,7 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; -import org.apache.fineract.infrastructure.event.business.domain.loan.repayment.LoanRepaymentOverdueBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.repayment.LoanRepaymentDueBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; @@ -68,8 +68,8 @@ public class CheckLoanRepaymentOverdueBusinessStepTest { @Test public void givenLoanWithInstallmentOverdueAfterConfiguredDaysWhenStepExecutionThenBusinessEventIsRaised() { - ArgumentCaptor loanRepaymentOverdueBusinessEventArgumentCaptor = ArgumentCaptor - .forClass(LoanRepaymentOverdueBusinessEvent.class); + ArgumentCaptor loanRepaymentDueBusinessEventArgumentCaptor = ArgumentCaptor + .forClass(LoanRepaymentDueBusinessEvent.class); // given when(configurationDomainService.retrieveRepaymentOverdueDays()).thenReturn(1L); LocalDate loanInstallmentRepaymentDueDate = DateUtils.getBusinessLocalDate().minusDays(1); @@ -83,8 +83,8 @@ public class CheckLoanRepaymentOverdueBusinessStepTest { // when Loan processedLoan = underTest.execute(loanForProcessing); // then - verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(loanRepaymentOverdueBusinessEventArgumentCaptor.capture()); - LoanRepaymentScheduleInstallment loanPayloadForEvent = loanRepaymentOverdueBusinessEventArgumentCaptor.getValue().get(); + verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(loanRepaymentDueBusinessEventArgumentCaptor.capture()); + LoanRepaymentScheduleInstallment loanPayloadForEvent = loanRepaymentDueBusinessEventArgumentCaptor.getValue().get(); assertEquals(repaymentInstallment, loanPayloadForEvent); assertEquals(processedLoan, loanForProcessing); } @@ -110,8 +110,6 @@ public class CheckLoanRepaymentOverdueBusinessStepTest { @Test public void givenLoanWithInstallmentOverdueAfterConfiguredDaysButInstallmentPaidOffWhenStepExecutionThenNoBusinessEvent() { - ArgumentCaptor loanRepaymentOverdueBusinessEventArgumentCaptor = ArgumentCaptor - .forClass(LoanRepaymentOverdueBusinessEvent.class); // given when(configurationDomainService.retrieveRepaymentOverdueDays()).thenReturn(1L); LocalDate loanInstallmentRepaymentDueDate = DateUtils.getBusinessLocalDate().minusDays(1); diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializerTest.java index 6415adf81..4fddeac00 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanRepaymentBusinessEventSerializerTest.java @@ -19,6 +19,8 @@ package org.apache.fineract.infrastructure.event.external.service.serialization.serializer.loan; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -31,9 +33,11 @@ import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import org.apache.avro.AvroRuntimeException; import org.apache.fineract.avro.generic.v1.CurrencyDataV1; import org.apache.fineract.avro.loan.v1.LoanRepaymentDueDataV1; import org.apache.fineract.avro.loan.v1.RepaymentDueDataV1; +import org.apache.fineract.avro.loan.v1.RepaymentPastDueDataV1; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -54,8 +58,11 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class LoanRepaymentBusinessEventSerializerTest { @Mock @@ -103,12 +110,52 @@ public class LoanRepaymentBusinessEventSerializerTest { // then CurrencyDataV1 currency = CurrencyDataV1.newBuilder().setCode("CODE").setDecimalPlaces(1).setInMultiplesOf(1).build(); - RepaymentDueDataV1 repaymentDue = new RepaymentDueDataV1(1, BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), + RepaymentDueDataV1 repaymentDue = new RepaymentDueDataV1(1, loanInstallmentRepaymentDueDate.format(DateTimeFormatter.ISO_DATE), + BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), + BigDecimal.valueOf(0.0)); + RepaymentPastDueDataV1 pastDue = new RepaymentPastDueDataV1(BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0)); - LoanRepaymentDueDataV1 expectedSerializedData = new LoanRepaymentDueDataV1(1L, "0001", "externalId", - loanInstallmentRepaymentDueDate.format(DateTimeFormatter.ISO_DATE), currency, BigDecimal.valueOf(0.0), repaymentDue); + LoanRepaymentDueDataV1 expectedSerializedData = new LoanRepaymentDueDataV1(1L, "0001", "externalId", currency, repaymentDue, + pastDue); assertEquals(data, expectedSerializedData); moneyHelper.close(); } + + @Test + public void testLoanRepaymentEventLoanIdMandatoryFieldValidation() { + // given + LoanRepaymentBusinessEventSerializer serializer = new LoanRepaymentBusinessEventSerializer(mapper); + + LocalDate loanInstallmentRepaymentDueDate = DateUtils.getBusinessLocalDate().plusDays(1); + + Loan loanForProcessing = Mockito.mock(Loan.class); + LoanProduct loanProduct = Mockito.mock(LoanProduct.class); + LoanSummary loanSummary = Mockito.mock(LoanSummary.class); + MonetaryCurrency loanCurrency = Mockito.mock(MonetaryCurrency.class); + MockedStatic moneyHelper = Mockito.mockStatic(MoneyHelper.class); + + LoanRepaymentScheduleInstallment repaymentInstallment = new LoanRepaymentScheduleInstallment(loanForProcessing, 1, + LocalDate.now(ZoneId.systemDefault()), loanInstallmentRepaymentDueDate, BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), + BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0)); + LoanRepaymentDueBusinessEvent event = new LoanRepaymentDueBusinessEvent(repaymentInstallment); + + // set mandatory loanID field as null + when(loanForProcessing.getId()).thenReturn(null); + when(loanForProcessing.getAccountNumber()).thenReturn("0001"); + when(loanForProcessing.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId")); + when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); + when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.valueOf(0.0)); + when(loanForProcessing.getCurrency()).thenReturn(loanCurrency); + when(loanCurrency.getCode()).thenReturn("CODE"); + when(loanCurrency.getCurrencyInMultiplesOf()).thenReturn(1); + when(loanCurrency.getDigitsAfterDecimal()).thenReturn(1); + when(mapper.mapLocalDate(any())).thenReturn(loanInstallmentRepaymentDueDate.format(DateTimeFormatter.ISO_DATE)); + moneyHelper.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP); + + // when + AvroRuntimeException exceptionThrown = assertThrows(AvroRuntimeException.class, () -> serializer.toAvroDTO(event)); + assertTrue(exceptionThrown.getMessage().contains("does not accept null values")); + moneyHelper.close(); + } }