diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDelinquencyRangeDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDelinquencyRangeDataV1.avsc index 3da294421d5..95d504c54b4 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDelinquencyRangeDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDelinquencyRangeDataV1.avsc @@ -50,6 +50,13 @@ "null", "string" ] + }, + { + "name": "installmentDelinquencyBuckets", + "type": { + "type": "array", + "items": "org.apache.fineract.avro.loan.v1.LoanInstallmentDelinquencyBucketDataV1" + } } ] } diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanInstallmentDelinquencyBucketDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanInstallmentDelinquencyBucketDataV1.avsc new file mode 100644 index 00000000000..22c306a2337 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanInstallmentDelinquencyBucketDataV1.avsc @@ -0,0 +1,31 @@ +{ + "name": "LoanInstallmentDelinquencyBucketDataV1", + "namespace": "org.apache.fineract.avro.loan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "delinquencyRange", + "type": [ + "null", + "org.apache.fineract.avro.loan.v1.DelinquencyRangeDataV1" + ] + }, + { + "name": "amount", + "doc": "Contains installments total, fee, interest, principal and penalty amount summaries", + "type": "org.apache.fineract.avro.loan.v1.LoanAmountDataV1" + }, + { + "name": "charges", + "type": { + "type": "array", + "items": "org.apache.fineract.avro.loan.v1.LoanChargeDataRangeViewV1" + } + }, + { + "name": "currency", + "type": "org.apache.fineract.avro.generic.v1.CurrencyDataV1" + } + ] +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanDelinquencyRangeChangeBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanDelinquencyRangeChangeBusinessEventSerializer.java index 7fb24d9eb22..fcf9d4045b5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanDelinquencyRangeChangeBusinessEventSerializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanDelinquencyRangeChangeBusinessEventSerializer.java @@ -19,8 +19,12 @@ package org.apache.fineract.infrastructure.event.external.service.serialization.serializer.loan; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.function.BiFunction; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.avro.generic.GenericContainer; import org.apache.fineract.avro.generator.ByteBufferSerializable; @@ -28,6 +32,8 @@ import org.apache.fineract.avro.loan.v1.LoanAccountDelinquencyRangeDataV1; import org.apache.fineract.avro.loan.v1.LoanAmountDataV1; import org.apache.fineract.avro.loan.v1.LoanChargeDataRangeViewV1; +import org.apache.fineract.avro.loan.v1.LoanInstallmentDelinquencyBucketDataV1; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanDelinquencyRangeChangeBusinessEvent; import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.generic.CurrencyDataMapper; @@ -36,10 +42,12 @@ import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.AvroDateTimeMapper; import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; import org.apache.fineract.portfolio.loanaccount.data.CollectionData; import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; @@ -94,6 +102,10 @@ public ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) { .build(); DelinquencyRangeDataV1 delinquencyRange = mapper.map(data.getDelinquencyRange()); + + List installmentsDelinquencyData = calculateInstallmentLevelDelinquencyData(event.get(), + data); + LoanAccountDelinquencyRangeDataV1.Builder builder = LoanAccountDelinquencyRangeDataV1.newBuilder(); return builder// .setLoanId(id)// @@ -104,7 +116,79 @@ public ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) { .setAmount(amount)// .setCurrency(currencyMapper.map(data.getCurrency()))// .setDelinquentDate(delinquentDate)// - .build(); + .setInstallmentDelinquencyBuckets(installmentsDelinquencyData).build(); + } + + private List calculateInstallmentLevelDelinquencyData(Loan loan, LoanAccountData data) { + List loanInstallmentDelinquencyData = new ArrayList<>(); + if (loan.isEnableInstallmentLevelDelinquency()) { + Collection installmentDelinquencyTags = delinquencyReadPlatformService + .retrieveLoanInstallmentsCurrentDelinquencyTag(loan.getId()); + if (installmentDelinquencyTags != null && installmentDelinquencyTags.size() > 0) { + // group installments that are in same range + Map> installmentsInSameRange = installmentDelinquencyTags.stream().collect( + Collectors.groupingBy(installmentDelnquencyTags -> installmentDelnquencyTags.getDelinquencyRange().getId())); + // for installments in each range, get details from loan repayment schedule installment, add amounts, + // list charges + for (Map.Entry> installmentDelinquencyTagData : installmentsInSameRange + .entrySet()) { + // get installments details + List delinquentInstallmentsInSameRange = loan.getRepaymentScheduleInstallments() + .stream().filter(installment -> installmentDelinquencyTagData.getValue().stream() + .anyMatch(installmentTag -> installmentTag.getId().equals(installment.getId()))) + .toList(); + // add amounts + LoanAmountDataV1 amount = LoanAmountDataV1.newBuilder()// + .setPrincipalAmount(delinquentInstallmentsInSameRange.stream() + .map(instlment -> instlment.getPrincipalOutstanding(loan.getCurrency()).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add))// + .setFeeAmount(delinquentInstallmentsInSameRange.stream() + .map(instlment -> instlment.getFeeChargesOutstanding(loan.getCurrency()).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add))// + .setInterestAmount(delinquentInstallmentsInSameRange.stream() + .map(instlment -> instlment.getInterestOutstanding(loan.getCurrency()).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add))// + .setPenaltyAmount(delinquentInstallmentsInSameRange.stream() + .map(instlment -> instlment.getPenaltyChargesOutstanding(loan.getCurrency()).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add))// + .setTotalAmount(delinquentInstallmentsInSameRange.stream() + .map(instlment -> instlment.getTotalOutstanding(loan.getCurrency()).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add))// + .build(); + + // get list of charges for installments in same range + List chargesForInstallmentsInSameRange = loan.getLoanCharges().stream() + .filter(loanCharge -> !loanCharge.isPaid() && delinquentInstallmentsInSameRange.stream().anyMatch( + installmentForCharge -> (DateUtils.isAfter(loanCharge.getDueDate(), installmentForCharge.getFromDate()) + || DateUtils.isEqual(loanCharge.getDueDate(), installmentForCharge.getFromDate())) + && (DateUtils.isBefore(loanCharge.getDueDate(), installmentForCharge.getDueDate()) + || DateUtils.isEqual(loanCharge.getDueDate(), installmentForCharge.getDueDate())))) + .toList(); + + List charges = new ArrayList<>(); + for (LoanCharge charge : chargesForInstallmentsInSameRange) { + LoanChargeDataRangeViewV1 chargeData = LoanChargeDataRangeViewV1.newBuilder().setId(charge.getId()) + .setName(charge.name()).setAmount(charge.amountOutstanding()) + .setCurrency(currencyMapper.map(data.getCurrency())).build(); + charges.add(chargeData); + } + + LoanInstallmentDelinquencyTagData.InstallmentDelinquencyRange delinquencyRange = installmentDelinquencyTagData + .getValue().get(0).getDelinquencyRange(); + + DelinquencyRangeDataV1 delinquencyRangeDataV1 = DelinquencyRangeDataV1.newBuilder().setId(delinquencyRange.getId()) + .setClassification(delinquencyRange.getClassification()).setMinimumAgeDays(delinquencyRange.getMinimumAgeDays()) + .setMaximumAgeDays(delinquencyRange.getMaximumAgeDays()).build(); + + LoanInstallmentDelinquencyBucketDataV1 installmentDelinquencyBucketDataV1 = LoanInstallmentDelinquencyBucketDataV1 + .newBuilder().setDelinquencyRange(delinquencyRangeDataV1).setAmount(amount).setCharges(charges) + .setCurrency(currencyMapper.map(data.getCurrency())).build(); + + loanInstallmentDelinquencyData.add(installmentDelinquencyBucketDataV1); + } + } + } + return loanInstallmentDelinquencyData; } private BigDecimal calculateDataSummary(Loan loan, BiFunction mapper) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/LoanInstallmentDelinquencyTagData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/LoanInstallmentDelinquencyTagData.java index 860156951e7..819db7384a9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/LoanInstallmentDelinquencyTagData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/LoanInstallmentDelinquencyTagData.java @@ -22,6 +22,8 @@ public interface LoanInstallmentDelinquencyTagData { + Long getId(); + InstallmentDelinquencyRange getDelinquencyRange(); BigDecimal getOutstandingAmount(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanInstallmentDelinquencyTagRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanInstallmentDelinquencyTagRepository.java index 5729fdec66e..24dc1721b75 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanInstallmentDelinquencyTagRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanInstallmentDelinquencyTagRepository.java @@ -38,7 +38,7 @@ public interface LoanInstallmentDelinquencyTagRepository List findByLoanId(@Param("loanId") Long loanId); // Fetching Installment Delinquency range and outstanding amount - @Query("select i.delinquencyRange, i.outstandingAmount from LoanInstallmentDelinquencyTag i where i.loan.id = :loanId") + @Query("select i.installment.id, i.delinquencyRange, i.outstandingAmount from LoanInstallmentDelinquencyTag i where i.loan.id = :loanId") List findInstallmentDelinquencyTags(@Param("loanId") Long loanId); @Modifying(flushAutomatically = true) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java index 75bcf3f7d61..2e88c76adc8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java @@ -175,12 +175,13 @@ public CommandProcessingResult applyDelinquencyTagToLoan(Long loanId, JsonComman final CollectionData collectionData = loanDelinquencyData.getLoanCollectionData(); // loan installments delinquent data final Map installmentsCollectionData = loanDelinquencyData.getLoanInstallmentsCollectionData(); - // delinquency for loan - changes = lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays()); // delinquency for installments if (installmentsCollectionData.size() > 0) { applyDelinquencyDetailsForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData); } + // delinquency for loan + changes = lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays()); + } return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loan.getId()) .withEntityExternalId(loan.getExternalId()).with(changes).build(); @@ -196,13 +197,13 @@ public void applyDelinquencyTagToLoan(LoanScheduleDelinquencyData loanDelinquenc final CollectionData collectionData = loanDelinquentData.getLoanCollectionData(); // loan installments delinquent data final Map installmentsCollectionData = loanDelinquentData.getLoanInstallmentsCollectionData(); - log.debug("Delinquency {}", collectionData); - // delinquency for loan - lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays()); // delinquency for installments if (installmentsCollectionData.size() > 0) { applyDelinquencyDetailsForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData); } + log.debug("Delinquency {}", collectionData); + // delinquency for loan + lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays()); } } @@ -231,10 +232,10 @@ public CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand @Override public void removeDelinquencyTagToLoan(final Loan loan) { - setLoanDelinquencyTag(loan, null); if (loan.isEnableInstallmentLevelDelinquency()) { cleanLoanInstallmentsDelinquencyTags(loan); } + setLoanDelinquencyTag(loan, null); } @Override diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java index 807c7cbbfb7..0002caec360 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java @@ -38,6 +38,7 @@ import java.util.Set; import java.util.stream.Collectors; import org.apache.fineract.avro.loan.v1.LoanAccountDelinquencyRangeDataV1; +import org.apache.fineract.avro.loan.v1.LoanInstallmentDelinquencyBucketDataV1; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.core.domain.ActionContext; import org.apache.fineract.infrastructure.core.domain.ExternalId; @@ -59,6 +60,7 @@ import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; import org.apache.fineract.portfolio.loanaccount.data.CollectionData; import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData; @@ -75,9 +77,12 @@ 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; import org.springframework.test.util.ReflectionTestUtils; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class LoanAccountDelinquencyRangeEventSerializerTest { @Mock @@ -101,7 +106,7 @@ public void setUp() { } @Test - public void testLoanRepaymentEventPayloadSerialization() throws IOException { + public void testLoanDelinquencyRangeEventPayloadSerialization() throws IOException { // given LoanDelinquencyRangeChangeBusinessEventSerializer serializer = new LoanDelinquencyRangeChangeBusinessEventSerializer( loanReadPlatformService, new LoanDelinquencyRangeDataMapperImpl(), loanChargeReadPlatformService, @@ -123,6 +128,7 @@ loanReadPlatformService, new LoanDelinquencyRangeDataMapperImpl(), loanChargeRea when(loanAccountData.getCurrency()).thenAnswer(a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), loanCurrency.getCurrencyInMultiplesOf())); when(loanForProcessing.getCurrency()).thenReturn(loanCurrency); + when(loanForProcessing.isEnableInstallmentLevelDelinquency()).thenReturn(false); when(delinquentData.getDelinquentDate()).thenReturn(delinquentDate); when(loanReadPlatformService.retrieveOne(any(Long.class))).thenReturn(loanAccountData); when(delinquencyReadPlatformService.calculateLoanCollectionData(any(Long.class))).thenReturn(delinquentData); @@ -163,6 +169,141 @@ loanReadPlatformService, new LoanDelinquencyRangeDataMapperImpl(), loanChargeRea moneyHelper.close(); } + @Test + public void testLoanDelinquencyRangeEventPayloadSerializationWithInstallmentDelinquencyData() throws IOException { + // given + LoanDelinquencyRangeChangeBusinessEventSerializer serializer = new LoanDelinquencyRangeChangeBusinessEventSerializer( + loanReadPlatformService, new LoanDelinquencyRangeDataMapperImpl(), loanChargeReadPlatformService, + delinquencyReadPlatformService, new LoanChargeDataMapperImpl(null, null, null), new CurrencyDataMapperImpl(), mapper); + + Loan loanForProcessing = Mockito.mock(Loan.class); + LoanAccountData loanAccountData = mock(LoanAccountData.class); + CollectionData delinquentData = mock(CollectionData.class); + MonetaryCurrency loanCurrency = new MonetaryCurrency("CODE", 1, 1); + MockedStatic moneyHelper = Mockito.mockStatic(MoneyHelper.class); + String delinquentDateAsStr = "2022-12-01"; + LocalDate delinquentDate = LocalDate.parse(delinquentDateAsStr); + when(loanForProcessing.getId()).thenReturn(1L); + when(loanAccountData.getId()).thenReturn(1L); + when(loanAccountData.getAccountNo()).thenReturn("0001"); + when(loanAccountData.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId")); + when(loanAccountData.getDelinquencyRange()).thenReturn(new DelinquencyRangeData(1L, "classification", 1, 10)); + when(loanAccountData.getCurrency()).thenAnswer(a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), + loanCurrency.getCurrencyInMultiplesOf())); + when(loanForProcessing.getCurrency()).thenReturn(loanCurrency); + when(loanForProcessing.isEnableInstallmentLevelDelinquency()).thenReturn(true); + when(delinquentData.getDelinquentDate()).thenReturn(delinquentDate); + when(loanReadPlatformService.retrieveOne(any(Long.class))).thenReturn(loanAccountData); + when(delinquencyReadPlatformService.calculateLoanCollectionData(any(Long.class))).thenReturn(delinquentData); + when(mapper.mapLocalDate(delinquentDate)).thenReturn(delinquentDateAsStr); + + LoanDelinquencyRangeChangeBusinessEvent event = new LoanDelinquencyRangeChangeBusinessEvent(loanForProcessing); + + List repaymentScheduleInstallments = new ArrayList<>(); + LoanRepaymentScheduleInstallment repaymentScheduleInstallment_1 = buildInstallment(loanForProcessing, loanCurrency, + BigDecimal.valueOf(100), BigDecimal.valueOf(10), BigDecimal.valueOf(20), BigDecimal.valueOf(20), BigDecimal.valueOf(150), + new BigDecimal("10"), new BigDecimal("20")); + when(repaymentScheduleInstallment_1.getId()).thenReturn(1L); + when(repaymentScheduleInstallment_1.getFromDate()).thenReturn(LocalDate.of(2022, 6, 20)); + when(repaymentScheduleInstallment_1.getDueDate()).thenReturn(LocalDate.of(2022, 6, 30)); + repaymentScheduleInstallments.add(repaymentScheduleInstallment_1); + + LoanRepaymentScheduleInstallment repaymentScheduleInstallment_2 = buildInstallment(loanForProcessing, loanCurrency, + BigDecimal.valueOf(100), BigDecimal.valueOf(0), BigDecimal.valueOf(20), BigDecimal.valueOf(0), BigDecimal.valueOf(120)); + when(repaymentScheduleInstallment_2.getId()).thenReturn(2L); + when(repaymentScheduleInstallment_2.getFromDate()).thenReturn(LocalDate.of(2022, 7, 1)); + when(repaymentScheduleInstallment_2.getDueDate()).thenReturn(LocalDate.of(2022, 7, 10)); + repaymentScheduleInstallments.add(repaymentScheduleInstallment_2); + + LoanRepaymentScheduleInstallment repaymentScheduleInstallment_3 = buildInstallment(loanForProcessing, loanCurrency, + BigDecimal.valueOf(100), BigDecimal.valueOf(0), BigDecimal.valueOf(20), BigDecimal.valueOf(0), BigDecimal.valueOf(120)); + when(repaymentScheduleInstallment_3.getId()).thenReturn(3L); + when(repaymentScheduleInstallment_3.getFromDate()).thenReturn(LocalDate.of(2022, 7, 11)); + when(repaymentScheduleInstallment_3.getDueDate()).thenReturn(LocalDate.of(2022, 7, 20)); + repaymentScheduleInstallments.add(repaymentScheduleInstallment_3); + + when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments); + when(loanChargeReadPlatformService.retrieveLoanCharges(anyLong())).thenAnswer(a -> repaymentScheduleInstallments.get(0) + .getInstallmentCharges().stream().map(c -> c.getLoanCharge().toData()).collect(Collectors.toList())); + + List installmentDelinquencyTags = new ArrayList<>(); + installmentDelinquencyTags.add(buildInstallmentDelinquencyTag(1L, 1L)); + installmentDelinquencyTags.add(buildInstallmentDelinquencyTag(2L, 1L)); + installmentDelinquencyTags.add(buildInstallmentDelinquencyTag(3L, 2L)); + + when(delinquencyReadPlatformService.retrieveLoanInstallmentsCurrentDelinquencyTag(anyLong())) + .thenReturn(installmentDelinquencyTags); + + when(loanForProcessing.getLoanCharges()).thenAnswer(a -> repaymentScheduleInstallments.get(0).getInstallmentCharges().stream() + .map(c -> c.getLoanCharge()).collect(Collectors.toList())); + + moneyHelper.when(() -> MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.UP); + + // when + LoanAccountDelinquencyRangeDataV1 data = (LoanAccountDelinquencyRangeDataV1) serializer.toAvroDTO(event); + + // then + assertEquals(1L, data.getLoanId()); + assertEquals("0001", data.getLoanAccountNo()); + assertEquals("externalId", data.getLoanExternalId()); + assertEquals(1L, data.getDelinquencyRange().getId()); + assertEquals("classification", data.getDelinquencyRange().getClassification()); + assertEquals(1, data.getDelinquencyRange().getMinimumAgeDays()); + assertEquals(10, data.getDelinquencyRange().getMaximumAgeDays()); + assertEquals(2, data.getCharges().size()); + assertTrue(data.getCharges().stream().anyMatch(a -> a.getAmount().compareTo(new BigDecimal("10")) == 0)); + assertTrue(data.getCharges().stream().anyMatch(a -> a.getAmount().compareTo(new BigDecimal("20")) == 0)); + assertEquals(0, data.getAmount().getTotalAmount().compareTo(new BigDecimal("390.0"))); + assertEquals(0, data.getAmount().getPrincipalAmount().compareTo(new BigDecimal("300.0"))); + assertEquals(0, data.getAmount().getInterestAmount().compareTo(new BigDecimal("60.0"))); + assertEquals(0, data.getAmount().getFeeAmount().compareTo(new BigDecimal("10.0"))); + assertEquals(0, data.getAmount().getPenaltyAmount().compareTo(new BigDecimal("20.0"))); + assertEquals(delinquentDateAsStr, data.getDelinquentDate()); + + // check installment delinquency data + assertEquals(2, data.getInstallmentDelinquencyBuckets().size()); + + // check calculations + + LoanInstallmentDelinquencyBucketDataV1 installmentDelinquencyBucketDataV1_1 = data.getInstallmentDelinquencyBuckets().get(0); + assertEquals(1L, installmentDelinquencyBucketDataV1_1.getDelinquencyRange().getId()); + assertEquals("range_1", installmentDelinquencyBucketDataV1_1.getDelinquencyRange().getClassification()); + assertEquals(0, installmentDelinquencyBucketDataV1_1.getAmount().getTotalAmount().compareTo(new BigDecimal("270.0"))); + assertEquals(0, installmentDelinquencyBucketDataV1_1.getAmount().getPrincipalAmount().compareTo(new BigDecimal("200.0"))); + assertEquals(0, installmentDelinquencyBucketDataV1_1.getAmount().getInterestAmount().compareTo(new BigDecimal("40.0"))); + assertEquals(0, installmentDelinquencyBucketDataV1_1.getAmount().getFeeAmount().compareTo(new BigDecimal("10.0"))); + assertEquals(0, installmentDelinquencyBucketDataV1_1.getAmount().getPenaltyAmount().compareTo(new BigDecimal("20.0"))); + assertEquals(2, installmentDelinquencyBucketDataV1_1.getCharges().size()); + assertTrue(installmentDelinquencyBucketDataV1_1.getCharges().stream() + .anyMatch(a -> a.getAmount().compareTo(new BigDecimal("10")) == 0)); + assertTrue(installmentDelinquencyBucketDataV1_1.getCharges().stream() + .anyMatch(a -> a.getAmount().compareTo(new BigDecimal("20")) == 0)); + + LoanInstallmentDelinquencyBucketDataV1 installmentDelinquencyBucketDataV1_2 = data.getInstallmentDelinquencyBuckets().get(1); + assertEquals(2L, installmentDelinquencyBucketDataV1_2.getDelinquencyRange().getId()); + assertEquals("range_2", installmentDelinquencyBucketDataV1_2.getDelinquencyRange().getClassification()); + assertEquals(0, installmentDelinquencyBucketDataV1_2.getAmount().getTotalAmount().compareTo(new BigDecimal("120.0"))); + assertEquals(0, installmentDelinquencyBucketDataV1_2.getAmount().getPrincipalAmount().compareTo(new BigDecimal("100.0"))); + assertEquals(0, installmentDelinquencyBucketDataV1_2.getAmount().getInterestAmount().compareTo(new BigDecimal("20.0"))); + assertEquals(0, installmentDelinquencyBucketDataV1_2.getAmount().getFeeAmount().compareTo(new BigDecimal("0.0"))); + assertEquals(0, installmentDelinquencyBucketDataV1_2.getAmount().getPenaltyAmount().compareTo(new BigDecimal("0.0"))); + assertEquals(0, installmentDelinquencyBucketDataV1_2.getCharges().size()); + moneyHelper.close(); + } + + private LoanInstallmentDelinquencyTagData buildInstallmentDelinquencyTag(long installmentId, long rangeId) { + LoanInstallmentDelinquencyTagData.InstallmentDelinquencyRange delinquencyRange = mock( + LoanInstallmentDelinquencyTagData.InstallmentDelinquencyRange.class); + when(delinquencyRange.getId()).thenReturn(rangeId); + when(delinquencyRange.getClassification()).thenReturn("range_" + rangeId); + when(delinquencyRange.getMaximumAgeDays()).thenReturn(1); + when(delinquencyRange.getMinimumAgeDays()).thenReturn(2); + LoanInstallmentDelinquencyTagData installmentDelinquencyTagData = mock(LoanInstallmentDelinquencyTagData.class); + when(installmentDelinquencyTagData.getId()).thenReturn(installmentId); + when(installmentDelinquencyTagData.getDelinquencyRange()).thenReturn(delinquencyRange); + return installmentDelinquencyTagData; + } + private LoanRepaymentScheduleInstallment buildInstallment(Loan loan, MonetaryCurrency currency, BigDecimal principalAmount, BigDecimal freeAmount, BigDecimal interestAmount, BigDecimal penaltyAmount, BigDecimal totalAmount, BigDecimal... charges) { @@ -173,6 +314,7 @@ private LoanRepaymentScheduleInstallment buildInstallment(Loan loan, MonetaryCur when(installment.getFeeChargesOutstanding(any())).thenAnswer(a -> Money.of(currency, freeAmount)); when(installment.getTotalOutstanding(any())).thenAnswer(a -> Money.of(currency, totalAmount)); Charge charge = mock(Charge.class); + when(charge.getName()).thenReturn("charge"); when(charge.toData()).thenAnswer(a -> { ChargeData chargeData = mock(ChargeData.class); when(chargeData.getCurrency()).thenAnswer(b -> new CurrencyData(currency.getCode())); @@ -193,7 +335,10 @@ private LoanInstallmentCharge buildLoanInstallmentCharge(BigDecimal amount, Char } private LoanCharge buildLoanCharge(Loan loan, BigDecimal amount, Charge charge) { - return new LoanCharge(loan, charge, amount, amount, ChargeTimeType.TRANCHE_DISBURSEMENT, ChargeCalculationType.FLAT, + LoanCharge loanCharge = new LoanCharge(loan, charge, amount, amount, ChargeTimeType.SPECIFIED_DUE_DATE, ChargeCalculationType.FLAT, LocalDate.of(2022, 6, 27), ChargePaymentMode.REGULAR, 1, new BigDecimal(100), ExternalId.generate()); + ReflectionTestUtils.setField(loanCharge, "id", 1L); + return loanCharge; } + }