FINERACT-1734-Deliquency-range-event

This commit is contained in:
Ruchi Dhamankar
2022-11-10 16:38:51 +01:00
committed by Arnold Gálovics
parent cb1d1d0d5d
commit 1cf37341d5
11 changed files with 406 additions and 5 deletions
@@ -0,0 +1,39 @@
{
"name": "LoanAccountDelinquencyRangeDataV1",
"namespace": "org.apache.fineract.avro.loan.v1",
"type": "record",
"fields": [
{
"default": null,
"name": "id",
"type": [
"null",
"long"
]
},
{
"default": null,
"name": "accountNo",
"type": [
"null",
"string"
]
},
{
"default": null,
"name": "externalId",
"type": [
"null",
"string"
]
},
{
"default": null,
"name": "delinquencyRange",
"type": [
"null",
"org.apache.fineract.avro.loan.v1.DelinquencyRangeDataV1"
]
}
]
}
@@ -5,14 +5,14 @@
"fields": [
{
"name": "fromLoanTransaction",
"type": "int"
"type": "long"
},
{
"default": null,
"name": "toLoanTransaction",
"type": [
"null",
"int"
"long"
]
},
{
@@ -20,7 +20,7 @@
"name": "toLoanCharge",
"type": [
"null",
"int"
"long"
]
},
{
@@ -0,0 +1,35 @@
/**
* 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;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
public class LoanDelinquencyRangeChangeBusinessEvent extends LoanBusinessEvent {
private static final String TYPE = "LoanDelinquencyRangeChangeBusinessEvent";
public LoanDelinquencyRangeChangeBusinessEvent(Loan value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
}
@@ -0,0 +1,30 @@
/**
* 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.mapper.loan;
import org.apache.fineract.avro.loan.v1.DelinquencyRangeDataV1;
import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.AvroMapperConfig;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
import org.mapstruct.Mapper;
@Mapper(config = AvroMapperConfig.class)
public interface LoanDelinquencyRangeDataMapper {
DelinquencyRangeDataV1 map(DelinquencyRangeData source);
}
@@ -0,0 +1,64 @@
/**
* 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 lombok.RequiredArgsConstructor;
import org.apache.avro.generic.GenericContainer;
import org.apache.fineract.avro.generator.ByteBufferSerializable;
import org.apache.fineract.avro.loan.v1.DelinquencyRangeDataV1;
import org.apache.fineract.avro.loan.v1.LoanAccountDelinquencyRangeDataV1;
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.loan.LoanDelinquencyRangeDataMapper;
import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.AbstractBusinessEventSerializer;
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class LoanDelinquencyRangeChangeBusinessEventSerializer extends AbstractBusinessEventSerializer {
private final LoanReadPlatformService service;
private final LoanDelinquencyRangeDataMapper mapper;
@Override
protected <T> ByteBufferSerializable toAvroDTO(BusinessEvent<T> rawEvent) {
LoanDelinquencyRangeChangeBusinessEvent event = (LoanDelinquencyRangeChangeBusinessEvent) rawEvent;
LoanAccountData data = service.retrieveOne(event.get().getId());
Long id = data.getId();
String accountNumber = data.getAccountNo();
String externalId = data.getExternalId();
DelinquencyRangeDataV1 delinquencyRange = mapper.map(data.getDelinquencyRange());
return new LoanAccountDelinquencyRangeDataV1(id, accountNumber, externalId, delinquencyRange);
}
@Override
public <T> boolean canSerialize(BusinessEvent<T> event) {
return event instanceof LoanDelinquencyRangeChangeBusinessEvent;
}
@Override
public Class<? extends GenericContainer> getSupportedSchema() {
return LoanAccountDelinquencyRangeDataV1.class;
}
}
@@ -33,6 +33,8 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanDelinquencyRangeChangeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiConstants;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
@@ -73,6 +75,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
private final LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository;
private final LoanRepositoryWrapper loanRepository;
private final LoanProductRepository loanProductRepository;
private final BusinessEventNotifierService businessEventNotifierService;
@Override
public CommandProcessingResult createDelinquencyRange(JsonCommand command) {
@@ -431,6 +434,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
}
if (loanDelinquencyTagHistory.size() > 0) {
this.loanDelinquencyTagRepository.saveAllAndFlush(loanDelinquencyTagHistory);
businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan));
}
return changes;
}
@@ -89,4 +89,5 @@
<include file="parts/0067_add_configurations_for_repayment_due_business_steps.xml" relativeToChangelogFile="true"/>
<include file="parts/0068_loan_charge_adjustment.xml" relativeToChangelogFile="true"/>
<include file="parts/0069_add_unique_constraint_for_reversal_external_id_of_loan_transactions.xml" relativeToChangelogFile="true"/>
<include file="parts/0070_add_event_configuration_for_delinquency_range_change_event.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet author="fineract" id="1">
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanDelinquencyRangeChangeBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -89,7 +89,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"SavingsCloseBusinessEvent", "SavingsCreateBusinessEvent", "SavingsDepositBusinessEvent",
"SavingsPostInterestBusinessEvent", "SavingsRejectBusinessEvent", "SavingsWithdrawalBusinessEvent",
"ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent",
"LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent");
"LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -162,7 +162,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"SavingsCloseBusinessEvent", "SavingsCreateBusinessEvent", "SavingsDepositBusinessEvent",
"SavingsPostInterestBusinessEvent", "SavingsRejectBusinessEvent", "SavingsWithdrawalBusinessEvent",
"ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent",
"LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent");
"LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent", "LoanDelinquencyRangeChangeBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -0,0 +1,191 @@
/**
* 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.portfolio.deliquency;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyIterable;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
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.LoanDelinquencyRangeChangeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappingsRepository;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceImpl;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class DelinquencyWritePlatformServiceRangeChangeEventTest {
@Mock
private DelinquencyBucketParseAndValidator dataValidatorBucket;
@Mock
private DelinquencyRangeParseAndValidator dataValidatorRange;
@Mock
private DelinquencyRangeRepository repositoryRange;
@Mock
private DelinquencyBucketRepository repositoryBucket;
@Mock
private DelinquencyBucketMappingsRepository repositoryBucketMappings;
@Mock
private LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository;
@Mock
private LoanRepositoryWrapper loanRepository;
@Mock
private LoanProductRepository loanProductRepository;
@Mock
private BusinessEventNotifierService businessEventNotifierService;
@InjectMocks
private DelinquencyWritePlatformServiceImpl underTest;
@BeforeEach
public void setUp() {
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil
.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault()))));
}
@Test
public void givenLoanAccountWithDelinquencyBucketWhenRangeChangeThenEventIsRaised() {
ArgumentCaptor<LoanDelinquencyRangeChangeBusinessEvent> loanDeliquencyRangeChangeEvent = ArgumentCaptor
.forClass(LoanDelinquencyRangeChangeBusinessEvent.class);
// given
Loan loanForProcessing = Mockito.mock(Loan.class);
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
range1.setId(1L);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
range2.setId(2L);
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
delinquencyBucket.setRanges(listDelinquencyRanges);
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket);
when(loanProduct.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loanProductRelatedDetail.getGraceOnArrearsAgeing()).thenReturn(1);
when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true);
when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty());
when(repositoryRange.getReferenceById(anyLong())).thenReturn(range1);
// when
underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData);
// then
verify(loanDelinquencyTagRepository, times(1)).saveAllAndFlush(anyIterable());
verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(loanDeliquencyRangeChangeEvent.capture());
Loan loanPayloadForEvent = loanDeliquencyRangeChangeEvent.getValue().get();
assertEquals(loanForProcessing, loanPayloadForEvent);
}
@Test
public void givenLoanAccountWithDelinquencyBucketWhenNoRangeChangeThenNoEventIsRaised() {
// given
Loan loanForProcessing = Mockito.mock(Loan.class);
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
range1.setId(1L);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
range2.setId(2L);
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
delinquencyBucket.setRanges(listDelinquencyRanges);
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate();
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 2L,
loanForProcessing);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket);
when(loanProduct.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loanProductRelatedDetail.getGraceOnArrearsAgeing()).thenReturn(1);
when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true);
when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty());
// when
underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData);
// then
verify(loanDelinquencyTagRepository, times(0)).saveAllAndFlush(anyIterable());
verify(businessEventNotifierService, times(0)).notifyPostBusinessEvent(any());
}
@Test
public void givenLoanAccountWithNoDelinquencyBucketThenNoEventIsRaised() {
// given
Loan loanForProcessing = Mockito.mock(Loan.class);
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate();
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 2L,
loanForProcessing);
when(loanForProcessing.hasDelinquencyBucket()).thenReturn(false);
// when
underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData);
// then
verify(loanDelinquencyTagRepository, times(0)).saveAllAndFlush(anyIterable());
verify(businessEventNotifierService, times(0)).notifyPostBusinessEvent(any());
}
}
@@ -435,6 +435,11 @@ public class ExternalEventConfigurationHelper {
loanChargeAdjustmentPreBusinessEvent.put("enabled", false);
defaults.add(loanChargeAdjustmentPreBusinessEvent);
Map<String, Object> loanDelinquencyRangeChangeBusinessEvent = new HashMap<>();
loanDelinquencyRangeChangeBusinessEvent.put("type", "LoanDelinquencyRangeChangeBusinessEvent");
loanDelinquencyRangeChangeBusinessEvent.put("enabled", false);
defaults.add(loanDelinquencyRangeChangeBusinessEvent);
return defaults;
}