FINERACT-1972 Custom Snapshot Event Triggered by COB
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java
index 6d07570..97aca96 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java
@@ -23,5 +23,7 @@
public static final String LIQUIBASE_ONLY = "liquibase-only";
public static final String DIAGNOSTICS = "diagnostics";
+ public static final String TEST = "test";
+
private FineractProfiles() {}
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/InternalExternalEventsApiResource.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/InternalExternalEventsApiResource.java
new file mode 100644
index 0000000..31b84f9
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/InternalExternalEventsApiResource.java
@@ -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();
+ }
+
+}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java
index 474384d..9f7ccbd 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java
@@ -26,11 +26,12 @@
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);
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/InternalExternalEventService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/InternalExternalEventService.java
new file mode 100644
index 0000000..b044b3d
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/InternalExternalEventService.java
@@ -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<>() {});
+ }
+
+}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/validation/ExternalEventDTO.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/validation/ExternalEventDTO.java
new file mode 100644
index 0000000..dec7037
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/validation/ExternalEventDTO.java
@@ -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;
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountCustomSnapshotBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountCustomSnapshotBusinessEvent.java
new file mode 100644
index 0000000..6284296
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountCustomSnapshotBusinessEvent.java
@@ -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;
+ }
+}
diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
index b08ead7..4d5fb3f 100644
--- a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
+++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
@@ -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>
diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1014_add_loan_account_custom_snapshot_event.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1014_add_loan_account_custom_snapshot_event.xml
new file mode 100644
index 0000000..4c1ad18
--- /dev/null
+++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1014_add_loan_account_custom_snapshot_event.xml
@@ -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>
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
index 7402955..c767a54 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
@@ -36,6 +36,7 @@
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.context.annotation.Profile;
import org.springframework.stereotype.Component;
-@Profile("test")
+@Profile(FineractProfiles.TEST)
@Component
@Path("/v1/internal/cob")
@RequiredArgsConstructor
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java
index 9d5d731..a925719 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java
@@ -35,13 +35,14 @@
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
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStep.java
new file mode 100644
index 0000000..fcce730
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStep.java
@@ -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";
+ }
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/api/InstanceModeApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/api/InstanceModeApiResource.java
index 215b754..837638f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/api/InstanceModeApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/api/InstanceModeApiResource.java
@@ -33,12 +33,13 @@
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")
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/s3/LocalstackS3ClientCustomizer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/s3/LocalstackS3ClientCustomizer.java
index 6b764aa..4e7d108 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/s3/LocalstackS3ClientCustomizer.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/s3/LocalstackS3ClientCustomizer.java
@@ -20,6 +20,7 @@
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 @@
@Component
@RequiredArgsConstructor
-@Profile("test")
+@Profile(FineractProfiles.TEST)
public class LocalstackS3ClientCustomizer implements S3ClientCustomizer {
private final Environment environment;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/InternalClientInformationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/InternalClientInformationApiResource.java
index 6b1342b..ae454ca 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/InternalClientInformationApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/InternalClientInformationApiResource.java
@@ -37,6 +37,7 @@
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.context.annotation.Profile;
import org.springframework.stereotype.Component;
-@Profile("test")
+@Profile(FineractProfiles.TEST)
@Component
@Path("/v1/internal/client")
@RequiredArgsConstructor
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java
index 7969fd9..a15d7f4 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java
@@ -38,6 +38,7 @@
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.context.annotation.Profile;
import org.springframework.stereotype.Component;
-@Profile("test")
+@Profile(FineractProfiles.TEST)
@Component
@Path("/v1/internal/loan")
@RequiredArgsConstructor
diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStepTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStepTest.java
new file mode 100644
index 0000000..161123c
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStepTest.java
@@ -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());
+ }
+
+}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
index ca46595..69b69d9 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
@@ -98,7 +98,7 @@
"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 @@
"LoanUndoChargeOffBusinessEvent", "LoanAccrualTransactionCreatedBusinessEvent",
"LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent",
"LoanTransactionDownPaymentPostBusinessEvent", "LoanTransactionDownPaymentPreBusinessEvent",
- "LoanAccountDelinquencyPauseChangedBusinessEvent");
+ "LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index e04e414..dcc4c1d 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -74,6 +74,8 @@
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 @@
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 @@
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) {
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
new file mode 100644
index 0000000..d2a29aa
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
@@ -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");
+ }
+
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
index 29bdab7..42c5e1d 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
@@ -495,6 +495,11 @@
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;
}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
index 5fc131b..261f7b9 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
@@ -283,6 +283,9 @@
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);
}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventHelper.java
new file mode 100644
index 0000000..793e144
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventHelper.java
@@ -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);
+ }
+
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventsExtension.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventsExtension.java
new file mode 100644
index 0000000..80befb0
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventsExtension.java
@@ -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));
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/CobHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/CobHelper.java
index 6b4188a..9e7bb38 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/CobHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/CobHelper.java
@@ -23,16 +23,17 @@
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);
}
}