FINERACT-1972 Custom Snapshot Event Triggered by COB

This commit is contained in:
Peter Bagrij
2024-01-22 15:29:21 +01:00
committed by Peter Bagrij
parent 4cb8946bc1
commit 8b379695d7
24 changed files with 1109 additions and 16 deletions
@@ -23,5 +23,7 @@ public final class FineractProfiles {
public static final String LIQUIBASE_ONLY = "liquibase-only";
public static final String DIAGNOSTICS = "diagnostics";
public static final String TEST = "test";
private FineractProfiles() {}
}
@@ -0,0 +1,80 @@
/**
* 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.api;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.event.external.service.InternalExternalEventService;
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Profile(FineractProfiles.TEST)
@Component
@Path("/v1/internal/externalevents")
@RequiredArgsConstructor
@Slf4j
public class InternalExternalEventsApiResource implements InitializingBean {
private final InternalExternalEventService internalExternalEventService;
private final DefaultToApiJsonSerializer<List<ExternalEventDTO>> jsonSerializer;
@Override
@SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
public void afterPropertiesSet() throws Exception {
log.warn("------------------------------------------------------------");
log.warn(" ");
log.warn("DO NOT USE THIS IN PRODUCTION!");
log.warn("Internal client services mode is enabled");
log.warn("DO NOT USE THIS IN PRODUCTION!");
log.warn(" ");
log.warn("------------------------------------------------------------");
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String getAllExternalEvents(@QueryParam("idempotencyKey") final String idempotencyKey, @QueryParam("type") final String type,
@QueryParam("category") final String category, @QueryParam("aggregateRootId") final Long aggregateRootId) {
log.debug("getAllExternalEvents called with params idempotencyKey:{}, type:{}, category:{}, aggregateRootId:{} ", idempotencyKey,
type, category, aggregateRootId);
List<ExternalEventDTO> allExternalEvents = internalExternalEventService.getAllExternalEvents(idempotencyKey, type, category,
aggregateRootId);
return jsonSerializer.serialize(allExternalEvents);
}
@DELETE
public void deleteAllExternalEvents() {
log.debug("deleteAllExternalEvents called");
internalExternalEventService.deleteAllExternalEvents();
}
}
@@ -26,11 +26,12 @@ import org.apache.fineract.infrastructure.event.external.repository.domain.Exter
import org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventView;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ExternalEventRepository extends JpaRepository<ExternalEvent, Long> {
public interface ExternalEventRepository extends JpaRepository<ExternalEvent, Long>, JpaSpecificationExecutor<ExternalEvent> {
List<ExternalEventView> findByStatusOrderById(ExternalEventStatus status, Pageable batchSize);
@@ -0,0 +1,147 @@
/**
* 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;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.avro.BulkMessageItemV1;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import org.apache.fineract.infrastructure.event.external.repository.ExternalEventRepository;
import org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEvent;
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
import org.springframework.context.annotation.Profile;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
@Service
@Profile(FineractProfiles.TEST)
@Slf4j
@AllArgsConstructor
public class InternalExternalEventService {
private final ExternalEventRepository externalEventRepository;
public void deleteAllExternalEvents() {
externalEventRepository.deleteAll();
}
public List<ExternalEventDTO> getAllExternalEvents(String idempotencyKey, String type, String category, Long aggregateRootId) {
List<Specification<ExternalEvent>> specifications = new ArrayList<>();
if (StringUtils.isNotEmpty(idempotencyKey)) {
specifications.add(hasIdempotencyKey(idempotencyKey));
}
if (StringUtils.isNotEmpty(type)) {
specifications.add(hasType(type));
}
if (StringUtils.isNotEmpty(category)) {
specifications.add(hasCategory(category));
}
if (aggregateRootId != null) {
specifications.add(hasAggregateRootId(aggregateRootId));
}
Specification<ExternalEvent> reducedSpecification = specifications.stream().reduce(Specification::and)
.orElse((Specification<ExternalEvent>) (root, query, criteriaBuilder) -> null);
List<ExternalEvent> externalEvents = externalEventRepository.findAll(reducedSpecification);
try {
return convertToReadableFormat(externalEvents);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException
| JsonProcessingException e) {
throw new RuntimeException("Error while converting external events to readable format", e);
}
}
private Specification<ExternalEvent> hasIdempotencyKey(String idempotencyKey) {
return (root, query, cb) -> cb.equal(root.get("idempotencyKey"), idempotencyKey);
}
private Specification<ExternalEvent> hasType(String type) {
return (root, query, cb) -> cb.equal(root.get("type"), type);
}
private Specification<ExternalEvent> hasCategory(String category) {
return (root, query, cb) -> cb.equal(root.get("category"), category);
}
private Specification<ExternalEvent> hasAggregateRootId(Long aggregateRootId) {
return (root, query, cb) -> cb.equal(root.get("aggregateRootId"), aggregateRootId);
}
private List<ExternalEventDTO> convertToReadableFormat(List<ExternalEvent> externalEvents) throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException, IllegalAccessException, JsonProcessingException {
List<ExternalEventDTO> eventMessages = new ArrayList<>();
for (ExternalEvent externalEvent : externalEvents) {
Class<?> payLoadClass = Class.forName(externalEvent.getSchema());
ByteBuffer byteBuffer = ByteBuffer.wrap(externalEvent.getData());
Method method = payLoadClass.getMethod("fromByteBuffer", ByteBuffer.class);
Object payLoad = method.invoke(null, byteBuffer);
if (externalEvent.getType().equalsIgnoreCase("BulkBusinessEvent")) {
Method methodToGetDatas = payLoad.getClass().getMethod("getDatas", (Class<?>) null);
List<BulkMessageItemV1> bulkMessages = (List<BulkMessageItemV1>) methodToGetDatas.invoke(payLoad);
StringBuilder bulkMessagePayload = new StringBuilder();
for (BulkMessageItemV1 bulkMessage : bulkMessages) {
ExternalEventDTO bulkMessageData = retrieveBulkMessage(bulkMessage, externalEvent);
bulkMessagePayload.append(bulkMessageData);
bulkMessagePayload.append(System.lineSeparator());
}
eventMessages.add(new ExternalEventDTO(externalEvent.getId(), externalEvent.getType(), externalEvent.getCategory(),
externalEvent.getCreatedAt(), toJsonMap(bulkMessagePayload.toString()), externalEvent.getBusinessDate(),
externalEvent.getSchema(), externalEvent.getAggregateRootId()));
} else {
eventMessages.add(new ExternalEventDTO(externalEvent.getId(), externalEvent.getType(), externalEvent.getCategory(),
externalEvent.getCreatedAt(), toJsonMap(payLoad.toString()), externalEvent.getBusinessDate(),
externalEvent.getSchema(), externalEvent.getAggregateRootId()));
}
}
return eventMessages;
}
private ExternalEventDTO retrieveBulkMessage(BulkMessageItemV1 messageItem, ExternalEvent externalEvent) throws ClassNotFoundException,
InvocationTargetException, IllegalAccessException, NoSuchMethodException, JsonProcessingException {
Class<?> messageBulkMessagePayLoad = Class.forName(messageItem.getDataschema());
Method methodForPayLoad = messageBulkMessagePayLoad.getMethod("fromByteBuffer", ByteBuffer.class);
Object payLoadBulkItem = methodForPayLoad.invoke(null, messageItem.getData());
return new ExternalEventDTO((long) messageItem.getId(), messageItem.getType(), messageItem.getCategory(),
externalEvent.getCreatedAt(), toJsonMap(payLoadBulkItem.toString()), externalEvent.getBusinessDate(),
externalEvent.getSchema(), externalEvent.getAggregateRootId());
}
private Map<String, Object> toJsonMap(String json) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(json, new TypeReference<>() {});
}
}
@@ -0,0 +1,41 @@
/**
* 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.validation;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
@Getter
@AllArgsConstructor
@ToString
public class ExternalEventDTO {
private final Long eventId;
private final String type;
private final String category;
private final OffsetDateTime createdAt;
private final Map<String, Object> payLoad;
private final LocalDate businessDate;
private final String schema;
private final Long aggregateRootId;
}
@@ -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 LoanAccountCustomSnapshotBusinessEvent extends LoanBusinessEvent {
private static final String TYPE = "LoanAccountCustomSnapshotBusinessEvent";
public LoanAccountCustomSnapshotBusinessEvent(Loan value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
}
@@ -36,4 +36,5 @@
<include relativeToChangelogFile="true" file="parts/1011_add_delinquency_actions_table.xml"/>
<include relativeToChangelogFile="true" file="parts/1012_introduce_loan_schedule_processing_type_configuration.xml"/>
<include relativeToChangelogFile="true" file="parts/1013_add_loan_account_delinquency_pause_changed_event.xml"/>
<include relativeToChangelogFile="true" file="parts/1014_add_loan_account_custom_snapshot_event.xml"/>
</databaseChangeLog>
@@ -0,0 +1,33 @@
<?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.1.xsd">
<changeSet author="fineract" id="1">
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanAccountCustomSnapshotBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -36,6 +36,7 @@ import org.apache.fineract.cob.loan.LoanCOBConstant;
import org.apache.fineract.cob.loan.RetrieveLoanIdService;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
@@ -43,7 +44,7 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Profile("test")
@Profile(FineractProfiles.TEST)
@Component
@Path("/v1/internal/cob")
@RequiredArgsConstructor
@@ -35,13 +35,14 @@ import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
import org.apache.fineract.cob.domain.LockOwner;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
@Profile("test")
@Profile(FineractProfiles.TEST)
@Component
@Path("/v1/internal/loans")
@RequiredArgsConstructor
@@ -0,0 +1,69 @@
/**
* 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.cob.loan;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountCustomSnapshotBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class CheckDueInstallmentsBusinessStep implements LoanCOBBusinessStep {
private final BusinessEventNotifierService businessEventNotifierService;
@Override
public Loan execute(Loan loan) {
log.debug("start processing custom snapshot event trigger business step loan for loan with id [{}]", loan.getId());
if (loan.getRepaymentScheduleInstallments() != null && loan.getRepaymentScheduleInstallments().size() > 0) {
final LocalDate currentDate = DateUtils.getBusinessLocalDate();
boolean shouldPostCustomSnapshotBusinessEvent = false;
for (int i = 0; i < loan.getRepaymentScheduleInstallments().size(); i++) {
if (loan.getRepaymentScheduleInstallments().get(i).getDueDate().equals(currentDate)
&& loan.getRepaymentScheduleInstallments().get(i).isNotFullyPaidOff()) {
shouldPostCustomSnapshotBusinessEvent = true;
}
}
if (shouldPostCustomSnapshotBusinessEvent) {
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountCustomSnapshotBusinessEvent(loan));
}
}
log.debug("end processing custom snapshot event trigger business step for loan with id [{}]", loan.getId());
return loan;
}
@Override
public String getEnumStyledName() {
return "CHECK_DUE_INSTALLMENTS";
}
@Override
public String getHumanReadableName() {
return "Check Due Installments";
}
}
@@ -33,12 +33,13 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Profile("test")
@Profile(FineractProfiles.TEST)
@Component
@Path("/v1/instance-mode")
@Tag(name = "Instance Mode", description = "Instance mode changing API")
@@ -20,6 +20,7 @@ package org.apache.fineract.infrastructure.s3;
import java.net.URI;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import org.apache.poi.util.StringUtil;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
@@ -28,7 +29,7 @@ import software.amazon.awssdk.services.s3.S3ClientBuilder;
@Component
@RequiredArgsConstructor
@Profile("test")
@Profile(FineractProfiles.TEST)
public class LocalstackS3ClientCustomizer implements S3ClientCustomizer {
private final Environment environment;
@@ -37,6 +37,7 @@ import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.portfolio.client.domain.Client;
@@ -45,7 +46,7 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Profile("test")
@Profile(FineractProfiles.TEST)
@Component
@Path("/v1/internal/client")
@RequiredArgsConstructor
@@ -38,6 +38,7 @@ import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -50,7 +51,7 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Profile("test")
@Profile(FineractProfiles.TEST)
@Component
@Path("/v1/internal/loan")
@RequiredArgsConstructor
@@ -0,0 +1,161 @@
/**
* 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.cob.loan;
import static org.mockito.Mockito.times;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.domain.ActionContext;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountCustomSnapshotBusinessEvent;
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;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
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.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class CheckDueInstallmentsBusinessStepTest {
@Mock
private BusinessEventNotifierService businessEventNotifierService;
@Captor
private ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor;
@InjectMocks
private CheckDueInstallmentsBusinessStep underTest;
/**
* Setup context before each test.
*/
@BeforeEach
public void setUp() {
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.parse("2024-01-16"),
BusinessDateType.COB_DATE, LocalDate.parse("2024-01-15"))));
}
@AfterEach
public void tearDown() {
ThreadLocalContextUtil.reset();
}
@Test
public void testNoRepaymentScheduleInLoan() {
// given
Loan loan = Mockito.mock(Loan.class);
// when
underTest.execute(loan);
// then
Mockito.verifyNoInteractions(businessEventNotifierService);
}
@Test
public void testInstallmentDueDateIsNotMatchingWithCurrentBusinessDate() {
// given
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getRepaymentScheduleInstallments()).thenReturn(List.of(Mockito.mock(LoanRepaymentScheduleInstallment.class)));
Mockito.when(loan.getRepaymentScheduleInstallments().get(0).getDueDate()).thenReturn(LocalDate.parse("2024-01-17"));
// when
underTest.execute(loan);
// then
Mockito.verifyNoInteractions(businessEventNotifierService);
}
@Test
public void testSingleInstallmentDueDateIsMatchingWithCurrentBusinessDateAndNotFullyPayed() {
// given
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getRepaymentScheduleInstallments()).thenReturn(List.of(Mockito.mock(LoanRepaymentScheduleInstallment.class)));
Mockito.when(loan.getRepaymentScheduleInstallments().get(0).getDueDate()).thenReturn(LocalDate.parse("2024-01-16"));
Mockito.when(loan.getRepaymentScheduleInstallments().get(0).isNotFullyPaidOff()).thenReturn(true);
// when
underTest.execute(loan);
// then
Mockito.verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(businessEventArgumentCaptor.capture());
BusinessEvent<?> rawEvent = businessEventArgumentCaptor.getValue();
Assertions.assertInstanceOf(LoanAccountCustomSnapshotBusinessEvent.class, rawEvent);
LoanAccountCustomSnapshotBusinessEvent event = (LoanAccountCustomSnapshotBusinessEvent) rawEvent;
Assertions.assertEquals(loan, event.get());
}
@Test
public void testSingleInstallmentDueDateIsMatchingWithCurrentBusinessDateAndFullyPayed() {
// given
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getRepaymentScheduleInstallments()).thenReturn(List.of(Mockito.mock(LoanRepaymentScheduleInstallment.class)));
Mockito.when(loan.getRepaymentScheduleInstallments().get(0).getDueDate()).thenReturn(LocalDate.parse("2024-01-16"));
Mockito.when(loan.getRepaymentScheduleInstallments().get(0).isNotFullyPaidOff()).thenReturn(false);
// when
underTest.execute(loan);
// then
Mockito.verifyNoInteractions(businessEventNotifierService);
}
@Test
public void testMultipleInstallmentDueDateIsMatchingWithCurrentBusinessDateAndNotFullyPayedButSingleEventIsGenerated() {
// given
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getRepaymentScheduleInstallments()).thenReturn(
List.of(Mockito.mock(LoanRepaymentScheduleInstallment.class), Mockito.mock(LoanRepaymentScheduleInstallment.class)));
// first one is a down payment installment
Mockito.when(loan.getRepaymentScheduleInstallments().get(0).getDueDate()).thenReturn(LocalDate.parse("2024-01-16"));
Mockito.when(loan.getRepaymentScheduleInstallments().get(0).isNotFullyPaidOff()).thenReturn(true);
Mockito.lenient().when(loan.getRepaymentScheduleInstallments().get(0).isDownPayment()).thenReturn(true);
// this one is a real installment
Mockito.when(loan.getRepaymentScheduleInstallments().get(1).getDueDate()).thenReturn(LocalDate.parse("2024-01-16"));
Mockito.when(loan.getRepaymentScheduleInstallments().get(1).isNotFullyPaidOff()).thenReturn(true);
// when
underTest.execute(loan);
// then
Mockito.verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(businessEventArgumentCaptor.capture());
BusinessEvent<?> rawEvent = businessEventArgumentCaptor.getValue();
Assertions.assertInstanceOf(LoanAccountCustomSnapshotBusinessEvent.class, rawEvent);
LoanAccountCustomSnapshotBusinessEvent event = (LoanAccountCustomSnapshotBusinessEvent) rawEvent;
Assertions.assertEquals(loan, event.get());
}
}
@@ -98,7 +98,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanChargeOffPostBusinessEvent", "LoanUndoChargeOffBusinessEvent", "LoanAccrualTransactionCreatedBusinessEvent",
"LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent",
"LoanTransactionDownPaymentPostBusinessEvent", "LoanTransactionDownPaymentPreBusinessEvent",
"LoanAccountDelinquencyPauseChangedBusinessEvent");
"LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -178,7 +178,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanUndoChargeOffBusinessEvent", "LoanAccrualTransactionCreatedBusinessEvent",
"LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent",
"LoanTransactionDownPaymentPostBusinessEvent", "LoanTransactionDownPaymentPreBusinessEvent",
"LoanAccountDelinquencyPauseChangedBusinessEvent");
"LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -74,6 +74,8 @@ import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -86,7 +88,7 @@ public abstract class BaseLoanIntegrationTest {
protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
protected final ResponseSpecification responseSpec = createResponseSpecification(200);
protected final ResponseSpecification responseSpec = createResponseSpecification(Matchers.is(200));
protected final RequestSpecification requestSpec = createRequestSpecification();
protected final AccountHelper accountHelper = new AccountHelper(requestSpec, responseSpec);
@@ -261,8 +263,8 @@ public abstract class BaseLoanIntegrationTest {
return request;
}
private static ResponseSpecification createResponseSpecification(int statusCode) {
return new ResponseSpecBuilder().expectStatusCode(statusCode).build();
protected static ResponseSpecification createResponseSpecification(Matcher<Integer> statusCodeMatcher) {
return new ResponseSpecBuilder().expectStatusCode(statusCodeMatcher).build();
}
protected void verifyUndoLastDisbursalShallFail(Long loanId, String expectedError) {
@@ -0,0 +1,326 @@
/**
* 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.integrationtests;
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
import com.google.gson.Gson;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.BusinessDateRequest;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
import org.apache.fineract.integrationtests.common.BusinessStepHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.ExternalEventConfigurationHelper;
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventsExtension;
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@Slf4j
@ExtendWith({ LoanTestLifecycleExtension.class, ExternalEventsExtension.class })
public class CustomSnapshotEventIntegrationTest extends BaseLoanIntegrationTest {
private SchedulerJobHelper schedulerJobHelper = new SchedulerJobHelper(this.requestSpec);
@Test
public void testSnapshotEventGenerationWhenLoanInstallmentIsNotPayed() {
runAt("31 January 2023", () -> {
// Enable Business Step
enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
"CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
"EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS");
enableLoanAccountCustomSnapshotBusinessEvent();
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023");
// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, false, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
installment(311.0, false, "01 May 2023") //
);
// delete all external events
deleteAllExternalEvents();
// run cob
updateBusinessDateAndExecuteCOBJob("01 February 2023");
// verify external events
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
Assertions.assertEquals(1, allExternalEvents.size());
Assertions.assertEquals("LoanAccountCustomSnapshotBusinessEvent", allExternalEvents.get(0).getType());
Assertions.assertEquals(loanId, allExternalEvents.get(0).getAggregateRootId());
});
}
@Test
public void testNoSnapshotEventGenerationWhenLoanInstallmentIsPayed() {
runAt("31 January 2023", () -> {
// Enable Business Step
enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
"CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
"EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS");
enableLoanAccountCustomSnapshotBusinessEvent();
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023");
// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, false, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
installment(311.0, false, "01 May 2023") //
);
addRepaymentForLoan(loanId, 313.0, "31 January 2023");
// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, true, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
installment(311.0, false, "01 May 2023") //
);
// delete all external events
deleteAllExternalEvents();
// run cob
updateBusinessDateAndExecuteCOBJob("01 February 2023");
// verify external events
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
Assertions.assertEquals(0, allExternalEvents.size());
});
}
@Test
public void testNoSnapshotEventGenerationWhenWhenCustomSnapshotEventCOBTaskIsNotActive() {
runAt("31 January 2023", () -> {
// Enable Business Step
enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
"CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
"EXTERNAL_ASSET_OWNER_TRANSFER");
enableLoanAccountCustomSnapshotBusinessEvent();
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023");
// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, false, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
installment(311.0, false, "01 May 2023") //
);
// delete all external events
deleteAllExternalEvents();
// run cob
updateBusinessDateAndExecuteCOBJob("01 February 2023");
// verify external events
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
Assertions.assertEquals(0, allExternalEvents.size());
});
}
@Test
public void testNoSnapshotEventGenerationWhenCOBDateIsNotMatchingWithInstallmentDueDate() {
runAt("30 January 2023", () -> {
// Enable Business Step
enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
"CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
"EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS");
enableLoanAccountCustomSnapshotBusinessEvent();
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023");
// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, false, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
installment(311.0, false, "01 May 2023") //
);
// delete all external events
deleteAllExternalEvents();
// run cob
updateBusinessDateAndExecuteCOBJob("31 January 2023");
// verify external events
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
Assertions.assertEquals(0, allExternalEvents.size());
});
}
@Test
public void testNoSnapshotEventGenerationWhenCustomSnapshotEventIsDisabled() {
runAt("31 January 2023", () -> {
// Enable Business Step
enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
"CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
"EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS");
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create Loan Product
PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023");
// Verify Repayment Schedule and Due Dates
verifyRepaymentSchedule(loanId, //
installment(0, null, "01 January 2023"), //
installment(313.0, false, "31 January 2023"), //
installment(313.0, false, "02 March 2023"), //
installment(313.0, false, "01 April 2023"), //
installment(311.0, false, "01 May 2023") //
);
// delete all external events
deleteAllExternalEvents();
// run cob
updateBusinessDateAndExecuteCOBJob("01 February 2023");
// verify external events
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
Assertions.assertEquals(0, allExternalEvents.size());
});
}
private void deleteAllExternalEvents() {
ExternalEventHelper.deleteAllExternalEvents(requestSpec, createResponseSpecification(Matchers.is(204)));
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
Assertions.assertEquals(0, allExternalEvents.size());
}
private void enableCOBBusinessStep(String... steps) {
new BusinessStepHelper().updateSteps("LOAN_CLOSE_OF_BUSINESS", steps);
}
public static String getExternalEventConfigurationsForUpdateJSON() {
Map<String, Map<String, Boolean>> configurationsForUpdate = new HashMap<>();
Map<String, Boolean> configurations = new HashMap<>();
configurations.put("CentersCreateBusinessEvent", true);
configurations.put("ClientActivateBusinessEvent", true);
configurationsForUpdate.put("externalEventConfigurations", configurations);
return new Gson().toJson(configurationsForUpdate);
}
private void enableLoanAccountCustomSnapshotBusinessEvent() {
final Map<String, Boolean> updatedConfigurations = ExternalEventConfigurationHelper.updateExternalEventConfigurations(requestSpec,
responseSpec, "{\"externalEventConfigurations\":{\"LoanAccountCustomSnapshotBusinessEvent\":true}}\n");
Assertions.assertEquals(updatedConfigurations.size(), 1);
Assertions.assertTrue(updatedConfigurations.containsKey("LoanAccountCustomSnapshotBusinessEvent"));
Assertions.assertTrue(updatedConfigurations.get("LoanAccountCustomSnapshotBusinessEvent"));
}
private void updateBusinessDateAndExecuteCOBJob(String date) {
businessDateHelper.updateBusinessDate(
new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
schedulerJobHelper.executeAndAwaitJob("Loan COB");
}
}
@@ -495,6 +495,11 @@ public class ExternalEventConfigurationHelper {
loanAccountDelinquencyPauseChangedBusinessEvent.put("enabled", false);
defaults.add(loanAccountDelinquencyPauseChangedBusinessEvent);
Map<String, Object> loanAccountCustomSnapshotBusinessEvent = new HashMap<>();
loanAccountCustomSnapshotBusinessEvent.put("type", "LoanAccountCustomSnapshotBusinessEvent");
loanAccountCustomSnapshotBusinessEvent.put("enabled", false);
defaults.add(loanAccountCustomSnapshotBusinessEvent);
return defaults;
}
@@ -283,6 +283,9 @@ public final class Utils {
final String deleteURL, final String jsonAttributeToGetBack) {
final String json = given().spec(requestSpec).expect().spec(responseSpec).log().ifError().when().delete(deleteURL).andReturn()
.asString();
if (jsonAttributeToGetBack == null) {
return (T) json;
}
return (T) JsonPath.from(json).get(jsonAttributeToGetBack);
}
@@ -0,0 +1,92 @@
/**
* 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.integrationtests.common.externalevents;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.util.List;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.util.JSON;
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
import org.apache.fineract.integrationtests.common.Utils;
@Slf4j
public final class ExternalEventHelper {
private static final Gson GSON = new JSON().getGson();
private ExternalEventHelper() {}
@Builder
public static class Filter {
private final String idempotencyKey;
private final String type;
private final String category;
private final Long aggregateRootId;
public String toQueryParams() {
StringBuilder stringBuilder = new StringBuilder();
if (idempotencyKey != null) {
stringBuilder.append("idempotencyKey=").append(idempotencyKey).append("&");
}
if (type != null) {
stringBuilder.append("type=").append(type).append("&");
}
if (category != null) {
stringBuilder.append("category=").append(category).append("&");
}
if (aggregateRootId != null) {
stringBuilder.append("aggregateRootId=").append(aggregateRootId).append("&");
}
return stringBuilder.toString();
}
}
public static List<ExternalEventDTO> getAllExternalEvents(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec) {
final String url = "/fineract-provider/api/v1/internal/externalevents?" + Utils.TENANT_IDENTIFIER;
log.info("---------------------------------GETTING ALL EXTERNAL EVENTS---------------------------------------------");
String response = Utils.performServerGet(requestSpec, responseSpec, url);
return GSON.fromJson(response, new TypeToken<List<ExternalEventDTO>>() {}.getType());
}
public static List<ExternalEventDTO> getAllExternalEvents(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec, Filter filter) {
final String url = "/fineract-provider/api/v1/internal/externalevents?" + filter.toQueryParams() + Utils.TENANT_IDENTIFIER;
log.info("---------------------------------GETTING ALL EXTERNAL EVENTS---------------------------------------------");
String response = Utils.performServerGet(requestSpec, responseSpec, url);
return GSON.fromJson(response, new TypeToken<List<ExternalEventDTO>>() {}.getType());
}
public static void deleteAllExternalEvents(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
final String url = "/fineract-provider/api/v1/internal/externalevents?" + Utils.TENANT_IDENTIFIER;
log.info("-----------------------------DELETE ALL EXTERNAL EVENTS PARTITIONS----------------------------------------");
Utils.performServerDelete(requestSpec, responseSpec, url, null);
}
}
@@ -0,0 +1,88 @@
/**
* 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.integrationtests.common.externalevents;
import static org.apache.fineract.integrationtests.common.Utils.initializeRESTAssured;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.util.ArrayList;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.integrationtests.common.ExternalEventConfigurationHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
@Slf4j
public class ExternalEventsExtension implements AfterEachCallback, BeforeEachCallback {
private Map<String, Boolean> original;
private ResponseSpecification responseSpec;
private RequestSpecification requestSpec;
public ExternalEventsExtension() {
initializeRESTAssured();
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.requestSpec.header("Fineract-Platform-TenantId", "default");
}
@Override
public void afterEach(ExtensionContext context) {
ArrayList<Map<String, Object>> allExternalEventConfigurations = ExternalEventConfigurationHelper
.getAllExternalEventConfigurations(requestSpec, responseSpec);
Map<String, Boolean> collected = allExternalEventConfigurations.stream()
.map(map -> Map.entry((String) map.get("type"), (Boolean) map.get("enabled")))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Map<String, MapDifference.ValueDifference<Boolean>> diff = Maps.difference(original, collected).entriesDiffering();
diff.keySet().forEach(key -> {
MapDifference.ValueDifference<Boolean> valueDifference = diff.get(key);
log.debug("External event {} changed from {} to {}. Restoring to its original state.", key, valueDifference.leftValue(),
valueDifference.rightValue());
restore(key, valueDifference.leftValue());
});
}
@Override
public void beforeEach(ExtensionContext context) {
ArrayList<Map<String, Object>> allExternalEventConfigurations = ExternalEventConfigurationHelper
.getAllExternalEventConfigurations(requestSpec, responseSpec);
original = allExternalEventConfigurations.stream().map(map -> Map.entry((String) map.get("type"), (Boolean) map.get("enabled")))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private void restore(String key, Boolean value) {
final Map<String, Boolean> updatedConfigurations = ExternalEventConfigurationHelper.updateExternalEventConfigurations(requestSpec,
responseSpec, "{\"externalEventConfigurations\":{\"" + key + "\":" + value + "}}\n");
Assertions.assertEquals(updatedConfigurations.size(), 1);
Assertions.assertTrue(updatedConfigurations.containsKey(key));
Assertions.assertEquals(value, updatedConfigurations.get(key));
}
}
@@ -23,16 +23,17 @@ import io.restassured.specification.ResponseSpecification;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.integrationtests.client.IntegrationTest;
import org.apache.fineract.integrationtests.common.Utils;
@Slf4j
public class CobHelper extends IntegrationTest {
public final class CobHelper {
private CobHelper() {}
public static List<Map<String, Object>> getCobPartitions(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec, int partitionSize, final String jsonReturn) {
final String GET_LOAN_URL = "/fineract-provider/api/v1/internal/cob/partitions/" + partitionSize + "?" + Utils.TENANT_IDENTIFIER;
final String url = "/fineract-provider/api/v1/internal/cob/partitions/" + partitionSize + "?" + Utils.TENANT_IDENTIFIER;
log.info("---------------------------------GET COB PARTITIONS---------------------------------------------");
return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_URL, jsonReturn);
return Utils.performServerGet(requestSpec, responseSpec, url, jsonReturn);
}
}