diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/CollectionDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/CollectionDataV1.avsc index 36256d227..3fdc726b7 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/CollectionDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/CollectionDataV1.avsc @@ -93,6 +93,17 @@ "items": "org.apache.fineract.avro.loan.v1.DelinquencyPausePeriodV1" } ] + }, + { + "default": null, + "name": "installmentDelinquencyBuckets", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.loan.v1.LoanInstallmentDelinquencyBucketDataV1" + } + ] } ] } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanAccountDataMapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanAccountDataMapper.java index 03b144614..d55a124f7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanAccountDataMapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/mapper/loan/LoanAccountDataMapper.java @@ -36,6 +36,7 @@ public interface LoanAccountDataMapper { @Mapping(target = "externalOwnerId", ignore = true) @Mapping(target = "settlementDate", ignore = true) @Mapping(target = "purchasePriceRatio", ignore = true) + @Mapping(target = "delinquent.installmentDelinquencyBuckets", ignore = true) LoanAccountDataV1 map(LoanAccountData source); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java index 2ed15944a..6844fe235 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java @@ -19,11 +19,13 @@ package org.apache.fineract.infrastructure.event.external.service.serialization.serializer.loan; import java.util.Collection; +import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.avro.generic.GenericContainer; import org.apache.commons.collections4.CollectionUtils; import org.apache.fineract.avro.generator.ByteBufferSerializable; import org.apache.fineract.avro.loan.v1.LoanAccountDataV1; +import org.apache.fineract.avro.loan.v1.LoanInstallmentDelinquencyBucketDataV1; import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusinessEvent; import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.loan.LoanAccountDataMapper; @@ -46,6 +48,7 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer { private final LoanAccountDataMapper mapper; private final LoanChargeReadPlatformService loanChargeReadPlatformService; private final DelinquencyReadPlatformService delinquencyReadPlatformService; + private final LoanInstallmentLevelDelinquencyEventProducer installmentLevelDelinquencyEventProducer; @Override public boolean canSerialize(BusinessEvent event) { @@ -74,7 +77,13 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer { } else { data.setSummary(LoanSummaryData.withOnlyCurrencyData(data.getCurrency())); } - return mapper.map(data); + + List installmentsDelinquencyData = installmentLevelDelinquencyEventProducer + .calculateInstallmentLevelDelinquencyData(event.get(), data.getCurrency()); + + LoanAccountDataV1 result = mapper.map(data); + result.getDelinquent().setInstallmentDelinquencyBuckets(installmentsDelinquencyData); + return result; } @Override 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 22a606c0a..01c7b0166 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,12 +19,8 @@ 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; @@ -33,7 +29,6 @@ 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; @@ -42,12 +37,10 @@ import org.apache.fineract.infrastructure.event.external.service.serialization.m 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; @@ -72,6 +65,7 @@ public class LoanDelinquencyRangeChangeBusinessEventSerializer implements Busine private final CurrencyDataMapper currencyMapper; private final AvroDateTimeMapper dataTimeMapper; + private final LoanInstallmentLevelDelinquencyEventProducer installmentLevelDelinquencyEventProducer; @Override public ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) { @@ -103,8 +97,8 @@ public class LoanDelinquencyRangeChangeBusinessEventSerializer implements Busine DelinquencyRangeDataV1 delinquencyRange = mapper.map(data.getDelinquencyRange()); - List installmentsDelinquencyData = calculateInstallmentLevelDelinquencyData(event.get(), - data); + List installmentsDelinquencyData = installmentLevelDelinquencyEventProducer + .calculateInstallmentLevelDelinquencyData(event.get(), data.getCurrency()); LoanAccountDelinquencyRangeDataV1.Builder builder = LoanAccountDelinquencyRangeDataV1.newBuilder(); return builder// @@ -119,79 +113,6 @@ public class LoanDelinquencyRangeChangeBusinessEventSerializer implements Busine .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.getEffectiveDueDate(), installmentForCharge.getFromDate()) - || DateUtils.isEqual(loanCharge.getEffectiveDueDate(), installmentForCharge.getFromDate())) - && (DateUtils.isBefore(loanCharge.getEffectiveDueDate(), installmentForCharge.getDueDate()) - || DateUtils.isEqual(loanCharge.getEffectiveDueDate(), 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) { return loan.getRepaymentScheduleInstallments().stream().map(installment -> mapper.apply(loan, installment)).reduce(BigDecimal.ZERO, BigDecimal::add); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanInstallmentLevelDelinquencyEventProducer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanInstallmentLevelDelinquencyEventProducer.java new file mode 100644 index 000000000..59d41753a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanInstallmentLevelDelinquencyEventProducer.java @@ -0,0 +1,121 @@ +/** + * 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.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.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.avro.loan.v1.DelinquencyRangeDataV1; +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.external.service.serialization.mapper.generic.CurrencyDataMapper; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData; +import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; +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.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class LoanInstallmentLevelDelinquencyEventProducer { + + private final DelinquencyReadPlatformService delinquencyReadPlatformService; + private final CurrencyDataMapper currencyMapper; + + public List calculateInstallmentLevelDelinquencyData(Loan loan, CurrencyData currency) { + 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(installment -> installment.getPrincipalOutstanding(loan.getCurrency()).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add))// + .setFeeAmount(delinquentInstallmentsInSameRange.stream() + .map(installment -> installment.getFeeChargesOutstanding(loan.getCurrency()).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add))// + .setInterestAmount(delinquentInstallmentsInSameRange.stream() + .map(installment -> installment.getInterestOutstanding(loan.getCurrency()).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add))// + .setPenaltyAmount(delinquentInstallmentsInSameRange.stream() + .map(installment -> installment.getPenaltyChargesOutstanding(loan.getCurrency()).getAmount()) + .reduce(BigDecimal.ZERO, BigDecimal::add))// + .setTotalAmount(delinquentInstallmentsInSameRange.stream() + .map(installment -> installment.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.getEffectiveDueDate(), installmentForCharge.getFromDate()) + || DateUtils.isEqual(loanCharge.getEffectiveDueDate(), installmentForCharge.getFromDate())) + && (DateUtils.isBefore(loanCharge.getEffectiveDueDate(), installmentForCharge.getDueDate()) + || DateUtils.isEqual(loanCharge.getEffectiveDueDate(), 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(currency)) + .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(currency)).build(); + + loanInstallmentDelinquencyData.add(installmentDelinquencyBucketDataV1); + } + } + } + return loanInstallmentDelinquencyData; + } +} 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 22a9aa378..62d8f49e2 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 @@ -116,7 +116,8 @@ public class LoanAccountDelinquencyRangeEventSerializerTest { // given LoanDelinquencyRangeChangeBusinessEventSerializer serializer = new LoanDelinquencyRangeChangeBusinessEventSerializer( loanReadPlatformService, new LoanDelinquencyRangeDataMapperImpl(), loanChargeReadPlatformService, - delinquencyReadPlatformService, new LoanChargeDataMapperImpl(null, null, null), new CurrencyDataMapperImpl(), mapper); + delinquencyReadPlatformService, new LoanChargeDataMapperImpl(null, null, null), new CurrencyDataMapperImpl(), mapper, + new LoanInstallmentLevelDelinquencyEventProducer(delinquencyReadPlatformService, new CurrencyDataMapperImpl())); Loan loanForProcessing = Mockito.mock(Loan.class); LoanAccountData loanAccountData = mock(LoanAccountData.class); @@ -180,7 +181,8 @@ public class LoanAccountDelinquencyRangeEventSerializerTest { // given LoanDelinquencyRangeChangeBusinessEventSerializer serializer = new LoanDelinquencyRangeChangeBusinessEventSerializer( loanReadPlatformService, new LoanDelinquencyRangeDataMapperImpl(), loanChargeReadPlatformService, - delinquencyReadPlatformService, new LoanChargeDataMapperImpl(null, null, null), new CurrencyDataMapperImpl(), mapper); + delinquencyReadPlatformService, new LoanChargeDataMapperImpl(null, null, null), new CurrencyDataMapperImpl(), mapper, + new LoanInstallmentLevelDelinquencyEventProducer(delinquencyReadPlatformService, new CurrencyDataMapperImpl())); Loan loanForProcessing = Mockito.mock(Loan.class); LoanAccountData loanAccountData = mock(LoanAccountData.class);