FINERACT-2386: Journal entry aggregation capabilities

This commit is contained in:
Adam Saghy
2025-10-03 11:07:12 +02:00
parent 297fa8e707
commit 89e9cfcd7d
35 changed files with 2531 additions and 3 deletions
@@ -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.infrastructure.jobs.service.aggregationjob;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.BDDMockito.given;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.migration.TenantDataSourceFactory;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.listener.JournalEntryAggregationJobListener;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.tasklet.JournalEntryAggregationTrackingTasklet;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.scope.context.JobSynchronizationManager;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.transaction.PlatformTransactionManager;
@EnableConfigurationProperties({ FineractProperties.class })
@ExtendWith(MockitoExtension.class)
class JournalEntryAggregationJobConfigurationTest {
@InjectMocks
private JournalEntryAggregationJobConfiguration configuration;
@Mock
JournalEntryAggregationJobListener journalEntryAggregationJobListener;
@Mock
private JobRepository jobRepository;
@Mock
private JournalEntryAggregationJobExecutionDecider journalEntryAggregationJobExecutionDecider;
@Mock
private JournalEntryAggregationJobWriter aggregationItemWriter;
@Mock
private PlatformTransactionManager transactionManager;
@Mock
private JournalEntryAggregationTrackingTasklet journalEntryAggregationTrackingTasklet;
@Mock
private TenantDataSourceFactory tenantDataSourceFactory;
@Mock
private FineractProperties fineractProperties;
@Mock
private FineractProperties.FineractJobProperties fineractJobProperties;
@Mock
private FineractProperties.FineractJournalEntryAggregationProperties journalEntryAggregationProperties;
@Mock
private JobExecution jobExecution;
@Mock
private ExecutionContext executionContext;
/**
* Test method for {@link JournalEntryAggregationJobConfiguration#journalEntryAggregation()}.
*/
@Test
public void testJournalEntryDailyAggregationJob() {
given(fineractProperties.getJob()).willReturn(fineractJobProperties);
given(fineractJobProperties.getJournalEntryAggregation()).willReturn(journalEntryAggregationProperties);
given(fineractJobProperties.getJournalEntryAggregation().getChunkSize()).willReturn(5);
JobSynchronizationManager.register(jobExecution);
assertNotNull(configuration.journalEntryAggregation(), "The journalEntryDailyAggregationJob bean should not be null");
}
}
@@ -0,0 +1,133 @@
/**
* 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.jobs.service.aggregationjob;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import java.time.Clock;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.stream.Stream;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.job.flow.FlowExecutionStatus;
import org.springframework.batch.item.ExecutionContext;
@ExtendWith(MockitoExtension.class)
public class JournalEntryAggregationJobExecutionDeciderTest {
final LocalDate aggregatedOnDate = LocalDate.now(ZoneId.of("UTC"));
@InjectMocks
private JournalEntryAggregationJobExecutionDecider decider;
@Mock
private JobExecution jobExecution;
@Mock
private StepExecution stepExecution;
@Mock
private ExecutionContext executionContext;
@Mock
private FineractProperties.FineractJobProperties fineractJobProperties;
@BeforeEach
public void setUp() {
when(jobExecution.getExecutionContext()).thenReturn(executionContext);
when(jobExecution.getExecutionContext().get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE)).thenReturn(aggregatedOnDate);
}
@Test
public void testDecideContinueExecution() {
// Arrange
LocalDate lastAggregatedOnDate = aggregatedOnDate.minusDays(1);
when(jobExecution.getExecutionContext().get(JournalEntryAggregationJobConstant.LAST_AGGREGATED_ON_DATE))
.thenReturn(lastAggregatedOnDate);
// Act
FlowExecutionStatus status = decider.decide(jobExecution, stepExecution);
// Assert
assertEquals(JournalEntryAggregationJobConstant.CONTINUE_JOB_EXECUTION, status.getName(),
"Should continue execution when lastAggregatedOnDate is before aggregatedOnDate");
}
@ParameterizedTest
@MethodSource("provideNoOpExecutionScenarios")
public void testDecideNoOpExecution(LocalDate lastAggregatedOnDate, String scenarioDescription) {
// Arrange
when(jobExecution.getExecutionContext().get(JournalEntryAggregationJobConstant.LAST_AGGREGATED_ON_DATE))
.thenReturn(lastAggregatedOnDate);
when(jobExecution.getExitStatus()).thenReturn(ExitStatus.NOOP);
// Act
FlowExecutionStatus status = decider.decide(jobExecution, stepExecution);
// Assert
assertEquals(JournalEntryAggregationJobConstant.NO_OP_EXECUTION, status.getName(),
"Should perform no-op execution when " + scenarioDescription);
assertEquals(ExitStatus.NOOP, jobExecution.getExitStatus(),
"Exit status should remain NOOP for scenario: " + scenarioDescription);
}
private static Stream<Arguments> provideNoOpExecutionScenarios() {
LocalDate aggregatedOnDate = LocalDate.now(Clock.systemUTC());
return Stream.of(Arguments.of(aggregatedOnDate, "lastAggregatedOnDate equals aggregatedOnDate"),
Arguments.of(aggregatedOnDate.plusDays(1), "lastAggregatedOnDate is after aggregatedOnDate"));
}
@Test
public void testDecideFirstTimeExecution() {
// Arrange
when(jobExecution.getExecutionContext().get(JournalEntryAggregationJobConstant.LAST_AGGREGATED_ON_DATE))
.thenReturn(null);
// Act
FlowExecutionStatus status = decider.decide(jobExecution, stepExecution);
// Assert
assertEquals(JournalEntryAggregationJobConstant.CONTINUE_JOB_EXECUTION, status.getName(),
"Should continue execution when no previous execution date exists (first time execution)");
}
@Test
public void testDecideContinueExecutionWithMultipleDaysDifference() {
// Arrange
LocalDate lastAggregatedOnDate = aggregatedOnDate.minusDays(5);
when(jobExecution.getExecutionContext().get(JournalEntryAggregationJobConstant.LAST_AGGREGATED_ON_DATE))
.thenReturn(lastAggregatedOnDate);
// Act
FlowExecutionStatus status = decider.decide(jobExecution, stepExecution);
// Assert
assertEquals(JournalEntryAggregationJobConstant.CONTINUE_JOB_EXECUTION, status.getName(),
"Should continue execution when lastAggregatedOnDate is multiple days before aggregatedOnDate");
}
}
@@ -0,0 +1,151 @@
/**
* 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.jobs.service.aggregationjob;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
import com.zaxxer.hikari.HikariDataSource;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.core.service.migration.TenantDataSourceFactory;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.data.JournalEntryAggregationSummaryData;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.scope.context.JobSynchronizationManager;
import org.springframework.batch.item.ExecutionContext;
@ExtendWith(MockitoExtension.class)
public class JournalEntryAggregationJobReaderTest {
@Mock
private FineractPlatformTenantConnection fineractPlatformTenantConnection;
@Mock
private TenantDataSourceFactory tenantDataSourceFactory;
@Mock
private JobExecution jobExecution;
@Mock
private ExecutionContext executionContext;
@Mock
private HikariDataSource dataSource;
@Mock
private ResultSet resultSet;
private JournalEntryAggregationJobReader reader;
private FineractPlatformTenant tenant;
@BeforeEach
public void setUp() {
tenant = new FineractPlatformTenant(1L, "default", "Default Tenant", "default", fineractPlatformTenantConnection);
ThreadLocalContextUtil.setTenant(tenant);
ThreadLocalContextUtil
.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault()))));
}
@AfterEach
public void tearDown() {
ThreadLocalContextUtil.reset();
if (JobSynchronizationManager.getContext() != null) {
JobSynchronizationManager.close();
}
}
@Test
public void testRowMapping() throws Exception {
// Arrange
setupResultSetMocks();
// Act
reader = new JournalEntryAggregationJobReader(tenantDataSourceFactory);
JournalEntryAggregationSummaryData result = invokeMapRowMethod(resultSet, 1);
// Assert
assertNotNull(result, "Mapped result should not be null");
assertEquals(Long.valueOf(1001L), result.getGlAccountId(), "GL Account ID should match");
assertEquals(100L, result.getProductId(), "Product ID should match");
assertEquals(Long.valueOf(1L), result.getOffice(), "Office should be 1L");
assertEquals(1L, result.getEntityTypeEnum(), "Entity type should be 1");
assertEquals("USD", result.getCurrencyCode(), "Currency code should match");
assertEquals(LocalDate.of(2023, 6, 15), result.getAggregatedOnDate(), "Aggregated date should match");
assertEquals(Long.valueOf(500L), result.getExternalOwnerId(), "Asset owner should match");
assertEquals(new BigDecimal("1000.00"), result.getDebitAmount(), "Debit amount should match");
assertEquals(Boolean.FALSE, result.getManualEntry(), "Manual entry should be false");
assertEquals(ThreadLocalContextUtil.getBusinessDate(), result.getSubmittedOnDate(), "Submitted on date should match business date");
}
@Test
public void testRowMappingWithNullAssetOwner() throws Exception {
// Arrange
setupResultSetMocksWithNullOwner();
// Act
reader = new JournalEntryAggregationJobReader(tenantDataSourceFactory);
JournalEntryAggregationSummaryData result = invokeMapRowMethod(resultSet, 1);
// Assert
assertEquals(Long.valueOf(0L), result.getExternalOwnerId(), "Asset owner should be 0 when null in resultset");
}
private void setupResultSetMocks() throws SQLException {
when(resultSet.getLong("glAccountId")).thenReturn(1001L);
when(resultSet.getLong("productId")).thenReturn(100L);
when(resultSet.getString("currencyCode")).thenReturn("USD");
when(resultSet.getDate("aggregatedOnDate")).thenReturn(Date.valueOf(LocalDate.of(2023,6,15)));
when(resultSet.findColumn("externalOwner")).thenReturn(5);
when(resultSet.getLong(5)).thenReturn(500L);
when(resultSet.getLong("officeId")).thenReturn(1L);
when(resultSet.getLong("entityTypeEnum")).thenReturn(1L);
when(resultSet.getBigDecimal("debitAmount")).thenReturn(new BigDecimal("1000.00"));
when(resultSet.getBigDecimal("creditAmount")).thenReturn(new BigDecimal("800.00"));
}
private void setupResultSetMocksWithNullOwner() throws SQLException {
when(resultSet.getLong("glAccountId")).thenReturn(1001L);
when(resultSet.getLong("productId")).thenReturn(100L);
when(resultSet.getString("currencyCode")).thenReturn("USD");
when(resultSet.getDate("aggregatedOnDate")).thenReturn(Date.valueOf(LocalDate.of(2023,6,15)));
when(resultSet.getLong("officeId")).thenReturn(1L);
when(resultSet.getLong("entityTypeEnum")).thenReturn(1L);
when(resultSet.getBigDecimal("debitAmount")).thenReturn(new BigDecimal("1000.00"));
when(resultSet.getBigDecimal("creditAmount")).thenReturn(new BigDecimal("800.00"));
}
private JournalEntryAggregationSummaryData invokeMapRowMethod(ResultSet rs, int rowNum) throws Exception {
Method mapRowMethod = JournalEntryAggregationJobReader.class.getDeclaredMethod("mapRow", ResultSet.class, int.class);
mapRowMethod.setAccessible(true);
return (JournalEntryAggregationSummaryData) mapRowMethod.invoke(reader, rs, rowNum);
}
}
@@ -0,0 +1,110 @@
/**
* 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.jobs.service.aggregationjob;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.data.JournalEntryAggregationSummaryData;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.services.JournalEntryAggregationWriterService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.item.Chunk;
@ExtendWith(MockitoExtension.class)
public class JournalEntryAggregationJobWriterTest {
@InjectMocks
private JournalEntryAggregationJobWriter writer;
@Mock
private JournalEntryAggregationWriterService writerService;
@Test
public void testWrite() {
// Arrange
JournalEntryAggregationSummaryData summaryData1 = mock(JournalEntryAggregationSummaryData.class);
JournalEntryAggregationSummaryData summaryData2 = mock(JournalEntryAggregationSummaryData.class);
Chunk<JournalEntryAggregationSummaryData> chunk = new Chunk<>(List.of(summaryData1, summaryData2));
StepExecution stepExecution = mock(StepExecution.class);
JobExecution jobExecution = mock(JobExecution.class);
when(stepExecution.getJobExecution()).thenReturn(jobExecution);
when(jobExecution.getId()).thenReturn(123L);
writer.beforeStep(stepExecution);
// Act
writer.write(chunk);
// Assert
verify(writerService, times(1)).insertJournalEntrySummaryBatch(anyList());
verify(summaryData1, times(1)).setJobExecutionId(123L);
verify(summaryData2, times(1)).setJobExecutionId(123L);
}
@Test
public void testWriteEmptyChunk() {
// Arrange
Chunk<JournalEntryAggregationSummaryData> emptyChunk = new Chunk<>();
StepExecution stepExecution = mock(StepExecution.class);
JobExecution jobExecution = mock(JobExecution.class);
when(stepExecution.getJobExecution()).thenReturn(jobExecution);
when(jobExecution.getId()).thenReturn(123L);
writer.beforeStep(stepExecution);
// Act
writer.write(emptyChunk);
// Assert
verify(writerService, times(1)).insertJournalEntrySummaryBatch(anyList());
}
@Test
public void testWriteSingleItem() {
// Arrange
JournalEntryAggregationSummaryData summaryData = mock(JournalEntryAggregationSummaryData.class);
Chunk<JournalEntryAggregationSummaryData> chunk = new Chunk<>(List.of(summaryData));
StepExecution stepExecution = mock(StepExecution.class);
JobExecution jobExecution = mock(JobExecution.class);
when(stepExecution.getJobExecution()).thenReturn(jobExecution);
when(jobExecution.getId()).thenReturn(456L);
writer.beforeStep(stepExecution);
// Act
writer.write(chunk);
// Assert
verify(writerService, times(1)).insertJournalEntrySummaryBatch(anyList());
verify(summaryData, times(1)).setJobExecutionId(456L);
}
}
@@ -0,0 +1,131 @@
/**
* 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.jobs.service.aggregationjob.listener;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenantConnection;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.JournalEntryAggregationJobConstant;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.domain.JournalEntryAggregationTrackingRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.scope.context.JobContext;
import org.springframework.batch.item.ExecutionContext;
@ExtendWith(MockitoExtension.class)
public class JournalEntryAggregationJobListenerTest {
private final LocalDate businessDate = LocalDate.now(ZoneId.of("UTC"));
@Mock
FineractPlatformTenantConnection fineractPlatformTenantConnection;
@Mock
private FineractProperties fineractProperties;
@Mock
private FineractProperties.FineractJobProperties fineractJobProperties;
@Mock
private FineractProperties.FineractJournalEntryAggregationProperties fineractJournalEntryAggregationProperties;
@Mock
private JournalEntryAggregationTrackingRepository trackingRepository;
@Mock
private JobExecution jobExecution;
@Mock
private JobContext jobContext;
@Mock
private ExecutionContext executionContext;
@InjectMocks
private JournalEntryAggregationJobListener listener;
@BeforeEach
public void setUp() {
ThreadLocalContextUtil
.setTenant(new FineractPlatformTenant(1L, "default", "Default Tenant", "default", fineractPlatformTenantConnection));
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.COB_DATE, businessDate.minusDays(1))));
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, businessDate)));
}
@Test
public void testBeforeJobWhenLastAggregatedOnDateNull() {
// Arrange
int lookBackDays = 2;
when(fineractProperties.getJob()).thenReturn(fineractJobProperties);
when(fineractJobProperties.getJournalEntryAggregation()).thenReturn(fineractJournalEntryAggregationProperties);
when(fineractJobProperties.getJournalEntryAggregation().getExcludeRecentNDays()).thenReturn(lookBackDays);
when(trackingRepository.findLatestAggregatedOnDate()).thenReturn(null);
ExecutionContext executionContext = new ExecutionContext();
when(jobExecution.getExecutionContext()).thenReturn(executionContext);
// Act
listener.beforeJob(jobExecution);
// Assert
assertEquals(businessDate.minusDays(lookBackDays), executionContext.get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE));
assertEquals(LocalDate.of(1970, 1, 1), executionContext.get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_FROM));
assertNull(executionContext.get(JournalEntryAggregationJobConstant.LAST_AGGREGATED_ON_DATE));
}
@Test
public void testBeforeJobWithLastAggregatedOnDate() {
// Arrange
LocalDate businessDate = LocalDate.now(ZoneId.of("UTC"));
LocalDate lastAggregatedOnDate = businessDate.minusDays(5);
int lookBackDays = 2;
when(fineractProperties.getJob()).thenReturn(fineractJobProperties);
when(fineractJobProperties.getJournalEntryAggregation()).thenReturn(fineractJournalEntryAggregationProperties);
when(fineractJobProperties.getJournalEntryAggregation().getExcludeRecentNDays()).thenReturn(lookBackDays);
when(trackingRepository.findLatestAggregatedOnDate()).thenReturn(lastAggregatedOnDate);
JobExecution jobExecution = new JobExecution(1L);
// Act
listener.beforeJob(jobExecution);
// Assert
assertEquals(businessDate.minusDays(lookBackDays),
jobExecution.getExecutionContext().get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE));
assertEquals(businessDate.minusDays(lookBackDays),
jobExecution.getExecutionContext().get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_TO));
assertEquals(lastAggregatedOnDate,
jobExecution.getExecutionContext().get(JournalEntryAggregationJobConstant.LAST_AGGREGATED_ON_DATE));
assertEquals(lastAggregatedOnDate,
jobExecution.getExecutionContext().get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_FROM));
}
}
@@ -0,0 +1,153 @@
/**
* 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.jobs.service.aggregationjob.services;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.data.JournalEntryAggregationSummaryData;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.data.JournalEntryAggregationTrackingData;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.domain.JournalEntryAggregationTrackingRepository;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.domain.JournalEntrySummary;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.domain.JournalEntrySummaryRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@ExtendWith(MockitoExtension.class)
public class JournalEntryAggregationWriterServiceImplTest {
@InjectMocks
private JournalEntryAggregationWriterServiceImpl writerService;
@Mock
private JournalEntrySummaryRepository journalSummaryRepository;
@Mock
private JournalEntryAggregationTrackingRepository journalEntryAggregationTrackingRepository;
@Mock
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Test
public void testInsertJournalEntryTracking() throws Exception {
// Arrange
JournalEntryAggregationTrackingData trackingData = mock(JournalEntryAggregationTrackingData.class);
// Act
writerService.insertJournalEntryTracking(trackingData);
// Assert
verify(journalEntryAggregationTrackingRepository, times(1)).save(any());
}
@Test
public void testRollbackJournalEntrySummary() {
// Arrange
Long jobExecutionId = 123L;
// Act
writerService.rollbackJournalEntrySummary(jobExecutionId);
// Assert
verify(journalSummaryRepository, times(1)).deleteByJobExecutionId(jobExecutionId);
}
@Test
public void testRollbackJournalEntryTracking() {
// Arrange
final Long jobExecutionId = 123L;
// Act
writerService.rollbackJournalEntryTracking(jobExecutionId);
// Assert
verify(journalEntryAggregationTrackingRepository, times(1)).deleteByJobExecutionId(jobExecutionId);
}
@Test
public void testInsertJournalEntrySummaryBatch() {
// Arrange
JournalEntryAggregationSummaryData summaryData1 = mock(JournalEntryAggregationSummaryData.class);
JournalEntryAggregationSummaryData summaryData2 = mock(JournalEntryAggregationSummaryData.class);
when(summaryData1.getAggregatedOnDate()).thenReturn(LocalDate.now(Clock.systemUTC()));
when(summaryData2.getAggregatedOnDate()).thenReturn(LocalDate.now(Clock.systemUTC()));
when(summaryData1.getJobExecutionId()).thenReturn(123L);
when(summaryData2.getJobExecutionId()).thenReturn(123L);
List<JournalEntryAggregationSummaryData> summariesList = Arrays.asList(summaryData1, summaryData2);
// Act
writerService.insertJournalEntrySummaryBatch(summariesList);
// Assert
verify(journalSummaryRepository, times(1)).saveAll(anyList());
}
@Test
public void testInsertJournalEntrySummaryBatchEmpty() {
// Arrange
List<JournalEntryAggregationSummaryData> emptySummariesList = List.of();
// Act
writerService.insertJournalEntrySummaryBatch(emptySummariesList);
// Assert
verify(journalSummaryRepository, times(1)).saveAll(anyList());
}
@Test
public void testInsertJournalEntrySummaryBatchWithAmounts() {
// Arrange
JournalEntryAggregationSummaryData summaryData = mock(JournalEntryAggregationSummaryData.class);
BigDecimal debitAmount = BigDecimal.valueOf(100.50);
BigDecimal creditAmount = BigDecimal.valueOf(200.75);
when(summaryData.getAggregatedOnDate()).thenReturn(LocalDate.now(Clock.systemUTC()));
when(summaryData.getDebitAmount()).thenReturn(debitAmount);
when(summaryData.getCreditAmount()).thenReturn(creditAmount);
when(summaryData.getJobExecutionId()).thenReturn(123L);
List<JournalEntryAggregationSummaryData> summariesList = List.of(summaryData);
// Act
writerService.insertJournalEntrySummaryBatch(summariesList);
// Assert
verify(journalSummaryRepository, times(1)).saveAll(argThat(entities -> {
if (!(entities instanceof List) || ((List<?>) entities).isEmpty()) {
return false;
}
JournalEntrySummary entity = (JournalEntrySummary) ((List<?>) entities).getFirst();
return entity.getDebitAmount().compareTo(debitAmount) == 0 && entity.getCreditAmount().compareTo(creditAmount) == 0;
}));
verify(summaryData, times(1)).getDebitAmount();
verify(summaryData, times(1)).getCreditAmount();
}
}
@@ -0,0 +1,136 @@
/**
* 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.jobs.service.aggregationjob.tasklet;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.time.Clock;
import java.time.LocalDate;
import java.util.Collections;
import java.util.HashMap;
import java.util.stream.Stream;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.domain.ActionContext;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.JournalEntryAggregationJobConstant;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.data.JournalEntryAggregationTrackingData;
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.services.JournalEntryAggregationWriterService;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.scope.context.StepContext;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.repeat.RepeatStatus;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class JournalEntryAggregationTrackingTaskletTest {
@Mock
private StepContribution stepContribution;
@Mock
private ChunkContext chunkContext;
@Mock
private StepContext stepContext;
@Mock
private StepExecution stepExecution;
@Mock
private JobExecution jobExecutionContext;
@Mock
private ExecutionContext executionContext;
@Mock
private JournalEntryAggregationWriterService journalEntryAggregationWriterService;
@InjectMocks
private JournalEntryAggregationTrackingTasklet underTest;
/**
* DataProvider for the testHappyPath method.
*
* @return the argument list
*/
private static Stream<Arguments> recordWriteCount() {
// spotless:off
return Stream.of(Arguments.of(1L), Arguments.of(0L));
// spotless:on
}
@ParameterizedTest
@MethodSource("recordWriteCount")
public void testHappyPath(final Long recordWriteCount) throws Exception {
given(stepContribution.getStepExecution()).willReturn(stepExecution);
given(stepExecution.getWriteCount()).willReturn(recordWriteCount);
doNothing().when(journalEntryAggregationWriterService).insertJournalEntryTracking(any(JournalEntryAggregationTrackingData.class));
given(chunkContext.getStepContext()).willReturn(stepContext);
given(stepContext.getStepExecution()).willReturn(stepExecution);
given(stepExecution.getJobExecution()).willReturn(jobExecutionContext);
given(jobExecutionContext.getExecutionContext()).willReturn(executionContext);
given(jobExecutionContext.getStepExecutions()).willReturn(Collections.singletonList(stepExecution));
given(stepExecution.getStepName()).willReturn(JournalEntryAggregationJobConstant.JOB_SUMMARY_STEP_NAME);
given(executionContext.get(any())).willReturn(LocalDate.now(Clock.systemUTC()));
final HashMap<BusinessDateType, LocalDate> businessDates = new HashMap<>();
final LocalDate cobDate = LocalDate.now(Clock.systemUTC());
businessDates.put(BusinessDateType.COB_DATE, cobDate);
businessDates.put(BusinessDateType.BUSINESS_DATE, cobDate.plusDays(1));
ThreadLocalContextUtil.setActionContext(ActionContext.COB);
ThreadLocalContextUtil.setBusinessDates(businessDates);
RepeatStatus repeatStatus = underTest.execute(stepContribution, chunkContext);
if (recordWriteCount > 0) {
verify(chunkContext, times(1)).getStepContext();
verify(stepContext, times(1)).getStepExecution();
verify(stepExecution, times(3)).getJobExecution();
verify(jobExecutionContext, times(2)).getExecutionContext();
verify(executionContext, times(2)).get(any());
verify(journalEntryAggregationWriterService, times(1))
.insertJournalEntryTracking(any(JournalEntryAggregationTrackingData.class));
} else {
verify(chunkContext, times(0)).getStepContext();
verify(stepContext, times(0)).getStepExecution();
verify(stepExecution, times(1)).getJobExecution();
verify(jobExecutionContext, times(0)).getExecutionContext();
verify(executionContext, times(0)).get(any());
}
assertEquals(RepeatStatus.FINISHED, repeatStatus);
}
}
@@ -104,6 +104,11 @@ fineract.jpa.statementLoggingEnabled=${FINERACT_STATEMENT_LOGGING_ENABLED:false}
fineract.database.defaultMasterPassword=${FINERACT_DEFAULT_MASTER_PASSWORD:fineract}
fineract.job.loan-cob-enabled=${FINERACT_JOB_LOAN_COB_ENABLED:true}
# Aggregation job configuration
fineract.job.journal-entry-aggregation.exclude-recent-N-days=${FINERACT_JOB_JOURNAL_ENTRY_AGGREGATION_EXCLUDE_RECENT_N_DAYS:1}
fineract.job.journal-entry-aggregation.enabled=${FINERACT_JOB_JOURNAL_ENTRY_AGGREGATION_ENABLED:true}
#this property if enabled, will create aggregated entry for all data on first run, instead of one entry per submitted_on_date
fineract.job.journal-entry-aggregation.chunk-size=${FINERACT_JOB_JOURNAL_ENTRY_AGGREGATION_CHUNK_SIZE:2000}
fineract.sampling.enabled=false
fineract.sampling.sampledClasses=