FINERACT-2386: Journal entry aggregation capabilities
This commit is contained in:
+1
-1
@@ -266,7 +266,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi
|
||||
scheduledJobDetails.setNextRunTime(null);
|
||||
final String stackTrace = getStackTraceAsString(throwable);
|
||||
scheduledJobDetails.setErrorLog(stackTrace);
|
||||
log.error("Could not schedule job: {}", scheduledJobDetails.getJobName(), throwable);
|
||||
log.warn("Could not schedule job: {}", scheduledJobDetails.getJobName(), throwable);
|
||||
}
|
||||
scheduledJobDetails.setCurrentlyRunning(false);
|
||||
}
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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.apache.fineract.infrastructure.jobs.service.aggregationjob.JournalEntryAggregationJobConstant.JOB_SUMMARY_STEP_NAME;
|
||||
import static org.apache.fineract.infrastructure.jobs.service.aggregationjob.JournalEntryAggregationJobConstant.JOB_TRACKING_STEP_NAME;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.apache.fineract.infrastructure.core.service.migration.TenantDataSourceFactory;
|
||||
import org.apache.fineract.infrastructure.jobs.service.JobName;
|
||||
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.data.JournalEntryAggregationSummaryData;
|
||||
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.listener.JournalEntryAggregationJobListener;
|
||||
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.tasklet.JournalEntryAggregationTrackingTasklet;
|
||||
import org.springframework.batch.core.Job;
|
||||
import org.springframework.batch.core.Step;
|
||||
import org.springframework.batch.core.job.builder.JobBuilder;
|
||||
import org.springframework.batch.core.launch.support.RunIdIncrementer;
|
||||
import org.springframework.batch.core.repository.JobRepository;
|
||||
import org.springframework.batch.core.step.builder.StepBuilder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(value = "fineract.job.journal-entry-aggregation.enabled", havingValue = "true")
|
||||
public class JournalEntryAggregationJobConfiguration {
|
||||
|
||||
@Autowired
|
||||
private JournalEntryAggregationJobListener journalEntryAggregationJobListener;
|
||||
@Autowired
|
||||
private JobRepository jobRepository;
|
||||
@Autowired
|
||||
private JournalEntryAggregationJobExecutionDecider journalEntryAggregationJobExecutionDecider;
|
||||
@Autowired
|
||||
private JournalEntryAggregationJobWriter aggregationItemWriter;
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
@Autowired
|
||||
private FineractProperties fineractProperties;
|
||||
@Autowired
|
||||
private JournalEntryAggregationTrackingTasklet journalEntryAggregationTrackingTasklet;
|
||||
@Autowired
|
||||
private TenantDataSourceFactory tenantDataSourceFactory;
|
||||
|
||||
@Bean
|
||||
public Step journalEntryAggregationSummaryStep() {
|
||||
return new StepBuilder(JOB_SUMMARY_STEP_NAME, jobRepository)
|
||||
.<JournalEntryAggregationSummaryData, JournalEntryAggregationSummaryData>chunk(
|
||||
fineractProperties.getJob().getJournalEntryAggregation().getChunkSize(), transactionManager)
|
||||
.reader(journalEntryAggregationJobReader()).writer(aggregationItemWriter).allowStartIfComplete(true).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JournalEntryAggregationJobReader journalEntryAggregationJobReader() {
|
||||
return new JournalEntryAggregationJobReader(tenantDataSourceFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
protected Step journalEntryAggregationTrackingStep() {
|
||||
return new StepBuilder(JOB_TRACKING_STEP_NAME, jobRepository).tasklet(journalEntryAggregationTrackingTasklet, transactionManager)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean(name = "journalEntryAggregation")
|
||||
public Job journalEntryAggregation() {
|
||||
return new JobBuilder(JobName.JOURNAL_ENTRY_AGGREGATION.name(), jobRepository).listener(journalEntryAggregationJobListener)
|
||||
.start(journalEntryAggregationJobExecutionDecider).on(JournalEntryAggregationJobConstant.NO_OP_EXECUTION).end()
|
||||
.from(journalEntryAggregationJobExecutionDecider).on(JournalEntryAggregationJobConstant.CONTINUE_JOB_EXECUTION)
|
||||
.to(journalEntryAggregationSummaryStep()).next(journalEntryAggregationTrackingStep()).end()
|
||||
.incrementer(new RunIdIncrementer()).build();
|
||||
}
|
||||
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public final class JournalEntryAggregationJobConstant {
|
||||
|
||||
public static final String CONTINUE_JOB_EXECUTION = "CONTINUE_JOB_EXECUTION";
|
||||
public static final String NO_OP_EXECUTION = "NO_OP_EXECUTION";
|
||||
public static final String JOURNAL_ENTRY_AGGREGATION_JOB_NAME = "JOURNAL_ENTRY_AGGREGATION";
|
||||
public static final String JOB_SUMMARY_STEP_NAME = "JournalEntryAggregation Summary Insert - Step";
|
||||
public static final String JOB_TRACKING_STEP_NAME = "JournalEntryAggregation Tracking Insert - Step";
|
||||
public static final String AGGREGATED_ON_DATE = "aggregatedOnDate";
|
||||
public static final String AGGREGATED_ON_DATE_FROM = "aggregatedOnDateFrom";
|
||||
public static final String AGGREGATED_ON_DATE_TO = "aggregatedOnDateTo";
|
||||
public static final String LAST_AGGREGATED_ON_DATE = "lastAggregatedOnDate";
|
||||
|
||||
private JournalEntryAggregationJobConstant() {}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.jobs.service.aggregationjob;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Objects;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.core.job.flow.JobExecutionDecider;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JournalEntryAggregationJobExecutionDecider implements JobExecutionDecider {
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public FlowExecutionStatus decide(final @NonNull JobExecution jobExecution, final StepExecution stepExecution) {
|
||||
final LocalDate aggregatedOnDate = (LocalDate) jobExecution.getExecutionContext()
|
||||
.get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE);
|
||||
final LocalDate lastAggregatedOnDate = (LocalDate) jobExecution.getExecutionContext()
|
||||
.get(JournalEntryAggregationJobConstant.LAST_AGGREGATED_ON_DATE);
|
||||
|
||||
// check if aggregation for given date range already exist.
|
||||
if (aggregationAlreadyExist(lastAggregatedOnDate, aggregatedOnDate)) {
|
||||
log.info(
|
||||
"Journal entry aggregation for given aggregatedOnDate already exist hence skipping jobName={} execution for aggregatedOnDate={}",
|
||||
JournalEntryAggregationJobConstant.JOURNAL_ENTRY_AGGREGATION_JOB_NAME, aggregatedOnDate);
|
||||
jobExecution.setExitStatus(ExitStatus.NOOP);
|
||||
return new FlowExecutionStatus(JournalEntryAggregationJobConstant.NO_OP_EXECUTION);
|
||||
}
|
||||
log.info("Continue executing journal entry aggregation job jobName={} execution for aggregatedOnDate={}",
|
||||
JournalEntryAggregationJobConstant.JOURNAL_ENTRY_AGGREGATION_JOB_NAME, aggregatedOnDate);
|
||||
return new FlowExecutionStatus(JournalEntryAggregationJobConstant.CONTINUE_JOB_EXECUTION);
|
||||
}
|
||||
|
||||
private boolean aggregationAlreadyExist(final LocalDate lastAggregatedOnDate, final LocalDate aggregatedOnDate) {
|
||||
if (Objects.isNull(lastAggregatedOnDate)) {
|
||||
log.info("Journal Entry aggregation job being executed for the first time");
|
||||
return false;
|
||||
}
|
||||
return aggregatedOnDate.isBefore(lastAggregatedOnDate) || aggregatedOnDate.isEqual(lastAggregatedOnDate);
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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 java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||
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.springframework.batch.core.StepExecution;
|
||||
import org.springframework.batch.core.annotation.BeforeStep;
|
||||
import org.springframework.batch.core.configuration.annotation.StepScope;
|
||||
import org.springframework.batch.item.ExecutionContext;
|
||||
import org.springframework.batch.item.database.JdbcCursorItemReader;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@StepScope
|
||||
public class JournalEntryAggregationJobReader extends JdbcCursorItemReader<JournalEntryAggregationSummaryData> {
|
||||
|
||||
private LocalDate aggregatedOnDateFrom;
|
||||
private LocalDate aggregatedOnDateTo;
|
||||
|
||||
public JournalEntryAggregationJobReader(TenantDataSourceFactory tenantDataSourceFactory) {
|
||||
FineractPlatformTenant tenant = ThreadLocalContextUtil.getTenant();
|
||||
setDataSource(tenantDataSourceFactory.create(tenant));
|
||||
setSql(buildAggregationQuery());
|
||||
setRowMapper(this::mapRow);
|
||||
}
|
||||
|
||||
@BeforeStep
|
||||
public void beforeStep(StepExecution stepExecution) {
|
||||
ExecutionContext ctx = stepExecution.getJobExecution().getExecutionContext();
|
||||
|
||||
this.aggregatedOnDateFrom = (LocalDate) ctx.get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_FROM);
|
||||
this.aggregatedOnDateTo = (LocalDate) ctx.get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_TO);
|
||||
|
||||
setPreparedStatementSetter(ps -> {
|
||||
ps.setObject(1, aggregatedOnDateFrom);
|
||||
ps.setObject(2, aggregatedOnDateTo);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private JournalEntryAggregationSummaryData mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
return JournalEntryAggregationSummaryData.builder() //
|
||||
.glAccountId(rs.getLong("glAccountId")) //
|
||||
.productId(rs.getLong("productId")) //
|
||||
.office(rs.getLong("officeId")) //
|
||||
.entityTypeEnum(rs.getLong("entityTypeEnum")) //
|
||||
.currencyCode(rs.getString("currencyCode")) //
|
||||
.submittedOnDate(ThreadLocalContextUtil.getBusinessDate()) //
|
||||
.aggregatedOnDate(JdbcSupport.getLocalDate(rs, "aggregatedOnDate")) //
|
||||
.externalOwnerId(JdbcSupport.getLong(rs, "externalOwner")) //
|
||||
.debitAmount(rs.getBigDecimal("debitAmount")) //
|
||||
.creditAmount(rs.getBigDecimal("creditAmount")) //
|
||||
.manualEntry(false) //
|
||||
.build(); //
|
||||
}
|
||||
|
||||
private String buildAggregationQuery() {
|
||||
return """
|
||||
SELECT lp.id AS productId,
|
||||
acc_gl_account.id AS glAccountId,
|
||||
acc_gl_journal_entry.entity_type_enum AS entityTypeEnum,
|
||||
acc_gl_journal_entry.office_id AS officeId,
|
||||
aw.owner_id AS externalOwner,
|
||||
SUM(CASE WHEN acc_gl_journal_entry.type_enum = 2 THEN amount ELSE 0 END) AS debitAmount,
|
||||
SUM(CASE WHEN acc_gl_journal_entry.type_enum = 1 THEN amount ELSE 0 END) AS creditAmount,
|
||||
acc_gl_journal_entry.submitted_on_date as aggregatedOnDate,
|
||||
acc_gl_journal_entry.currency_code as currencyCode
|
||||
FROM acc_gl_account
|
||||
JOIN acc_gl_journal_entry ON acc_gl_account.id = acc_gl_journal_entry.account_id
|
||||
JOIN m_loan m ON m.id = acc_gl_journal_entry.entity_id
|
||||
JOIN m_product_loan lp ON lp.id = m.product_id
|
||||
LEFT JOIN m_external_asset_owner_journal_entry_mapping aw
|
||||
ON aw.journal_entry_id = acc_gl_journal_entry.id
|
||||
WHERE acc_gl_journal_entry.entity_type_enum = 1
|
||||
AND acc_gl_journal_entry.submitted_on_date > ?
|
||||
AND acc_gl_journal_entry.submitted_on_date <= ?
|
||||
GROUP BY productId, glAccountId, externalOwner, aggregatedOnDate, currencyCode, entityTypeEnum, officeId
|
||||
""";
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.data.JournalEntryAggregationSummaryData;
|
||||
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.services.JournalEntryAggregationWriterService;
|
||||
import org.springframework.batch.core.StepExecution;
|
||||
import org.springframework.batch.core.StepExecutionListener;
|
||||
import org.springframework.batch.item.Chunk;
|
||||
import org.springframework.batch.item.ItemWriter;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class JournalEntryAggregationJobWriter implements ItemWriter<JournalEntryAggregationSummaryData>, StepExecutionListener {
|
||||
|
||||
private final JournalEntryAggregationWriterService journalEntryAggregationWriterService;
|
||||
private StepExecution stepExecution;
|
||||
|
||||
@Override
|
||||
public void beforeStep(@NonNull StepExecution stepExecution) {
|
||||
this.stepExecution = stepExecution;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(@NonNull Chunk<? extends JournalEntryAggregationSummaryData> journalEntrySummaries) {
|
||||
final Long jobExecutionId = stepExecution.getJobExecution().getId();
|
||||
List<JournalEntryAggregationSummaryData> summariesList = journalEntrySummaries.getItems().stream().map(item -> {
|
||||
item.setJobExecutionId(jobExecutionId);
|
||||
return (JournalEntryAggregationSummaryData) item;
|
||||
}).toList();
|
||||
journalEntryAggregationWriterService.insertJournalEntrySummaryBatch(summariesList);
|
||||
}
|
||||
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder(toBuilder = true)
|
||||
public class JournalEntryAggregationSummaryData {
|
||||
|
||||
private Long productId;
|
||||
private Long glAccountId;
|
||||
private Long office;
|
||||
private Long entityTypeEnum;
|
||||
private LocalDate submittedOnDate;
|
||||
private LocalDate aggregatedOnDate;
|
||||
private Long externalOwnerId;
|
||||
private BigDecimal debitAmount;
|
||||
private BigDecimal creditAmount;
|
||||
private Boolean manualEntry;
|
||||
private String currencyCode;
|
||||
private Long jobExecutionId;
|
||||
}
|
||||
+35
@@ -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.jobs.service.aggregationjob.data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder(toBuilder = true)
|
||||
public class JournalEntryAggregationTrackingData {
|
||||
|
||||
private LocalDate aggregatedOnDateFrom;
|
||||
private LocalDate aggregatedOnDateTo;
|
||||
private LocalDate submittedOnDate;
|
||||
private Long jobExecutionId;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_journal_entry_aggregation_tracking")
|
||||
@Getter
|
||||
@Setter
|
||||
public class JournalEntryAggregationTracking extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@Column(name = "aggregated_on_date_from", nullable = false)
|
||||
private LocalDate aggregatedOnDateFrom;
|
||||
|
||||
@Column(name = "aggregated_on_date_to", nullable = false)
|
||||
private LocalDate aggregatedOnDateTo;
|
||||
|
||||
@Column(name = "submitted_on_date", nullable = false)
|
||||
private LocalDate submittedOnDate;
|
||||
|
||||
@Column(name = "job_execution_id", nullable = false)
|
||||
private Long jobExecutionId;
|
||||
|
||||
}
|
||||
+35
@@ -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.jobs.service.aggregationjob.domain;
|
||||
|
||||
import java.time.LocalDate;
|
||||
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;
|
||||
|
||||
public interface JournalEntryAggregationTrackingRepository
|
||||
extends JpaRepository<JournalEntryAggregationTracking, Long>, JpaSpecificationExecutor<JournalEntryAggregationTracking> {
|
||||
|
||||
@Query("select max(jeat.aggregatedOnDateTo) from JournalEntryAggregationTracking jeat")
|
||||
LocalDate findLatestAggregatedOnDate();
|
||||
|
||||
@Modifying
|
||||
void deleteByJobExecutionId(Long jobExecutionId);
|
||||
}
|
||||
+69
@@ -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.infrastructure.jobs.service.aggregationjob.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@Table(name = "m_journal_entry_aggregation_summary")
|
||||
public class JournalEntrySummary extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long product;
|
||||
|
||||
@Column(name = "gl_account_id", nullable = false)
|
||||
private Long glAccountId;
|
||||
|
||||
@Column(name = "office_id", nullable = false)
|
||||
private Long office;
|
||||
|
||||
@Column(name = "entity_type_enum", nullable = false)
|
||||
private Long entityTypeEnum;
|
||||
|
||||
@Column(name = "aggregated_on_date", nullable = false)
|
||||
private LocalDate aggregatedOnDate;
|
||||
|
||||
@Column(name = "submitted_on_date", nullable = false)
|
||||
private LocalDate submittedOnDate;
|
||||
|
||||
@Column(name = "external_owner_id", nullable = false)
|
||||
private Long externalOwnerId;
|
||||
|
||||
@Column(name = "debit_amount")
|
||||
private BigDecimal debitAmount;
|
||||
|
||||
@Column(name = "credit_amount")
|
||||
private BigDecimal creditAmount;
|
||||
|
||||
@Column(name = "manual_entry", nullable = false)
|
||||
private Boolean manualEntry = false;
|
||||
|
||||
@Column(name = "job_execution_id", nullable = false)
|
||||
private Long jobExecutionId;
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.domain;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
|
||||
public interface JournalEntrySummaryRepository extends JpaRepository<JournalEntrySummary, Long> {
|
||||
|
||||
@Modifying
|
||||
void deleteByJobExecutionId(Long jobExecutionId);
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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.apache.fineract.infrastructure.jobs.service.aggregationjob.JournalEntryAggregationJobConstant.JOB_SUMMARY_STEP_NAME;
|
||||
import static org.apache.fineract.infrastructure.jobs.service.aggregationjob.JournalEntryAggregationJobConstant.JOURNAL_ENTRY_AGGREGATION_JOB_NAME;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
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.apache.fineract.infrastructure.jobs.service.aggregationjob.services.JournalEntryAggregationWriterService;
|
||||
import org.springframework.batch.core.ExitStatus;
|
||||
import org.springframework.batch.core.JobExecution;
|
||||
import org.springframework.batch.core.JobExecutionListener;
|
||||
import org.springframework.batch.core.StepExecution;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class JournalEntryAggregationJobListener implements JobExecutionListener {
|
||||
|
||||
private static final List<String> successJobStatus = List.of(ExitStatus.COMPLETED.getExitCode(), ExitStatus.NOOP.getExitCode());
|
||||
|
||||
private final FineractProperties fineractProperties;
|
||||
private final JournalEntryAggregationTrackingRepository journalEntryAggregationTrackingRepository;
|
||||
private final JournalEntryAggregationWriterService journalEntryAggregationWriterService;
|
||||
|
||||
@Override
|
||||
public void beforeJob(final JobExecution jobExecution) {
|
||||
log.info("Journal Entry Aggregation Job Started jobName={}, jobExecutionId={}", JOURNAL_ENTRY_AGGREGATION_JOB_NAME,
|
||||
jobExecution.getId());
|
||||
LocalDate providedAggregatedOnDate = (LocalDate) jobExecution.getExecutionContext()
|
||||
.get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE);
|
||||
// if aggregatedOnDate not provided in the parameter it will be defaulted to businessDate.
|
||||
final LocalDate aggregatedOnDate = providedAggregatedOnDate != null //
|
||||
? providedAggregatedOnDate.minusDays(fineractProperties.getJob().getJournalEntryAggregation().getExcludeRecentNDays()) //
|
||||
: ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.BUSINESS_DATE)
|
||||
.minusDays(fineractProperties.getJob().getJournalEntryAggregation().getExcludeRecentNDays()); //
|
||||
// get last or most recent aggregatedOnDate from tacking table.
|
||||
final LocalDate lastAggregatedOnDate = journalEntryAggregationTrackingRepository.findLatestAggregatedOnDate();
|
||||
|
||||
initializeDates(jobExecution, aggregatedOnDate, lastAggregatedOnDate);
|
||||
}
|
||||
|
||||
private void initializeDates(final JobExecution jobExecution, final LocalDate aggregatedOnDate, final LocalDate lastAggregatedOnDate) {
|
||||
jobExecution.getExecutionContext().put(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE, aggregatedOnDate);
|
||||
jobExecution.getExecutionContext().put(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_TO, aggregatedOnDate);
|
||||
if (lastAggregatedOnDate == null) {
|
||||
// Job is running for the first time
|
||||
jobExecution.getExecutionContext().put(JournalEntryAggregationJobConstant.LAST_AGGREGATED_ON_DATE, null);
|
||||
jobExecution.getExecutionContext().put(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_FROM, LocalDate.of(1970, 1, 1));
|
||||
} else {
|
||||
// Job is running for the subsequent time.
|
||||
jobExecution.getExecutionContext().put(JournalEntryAggregationJobConstant.LAST_AGGREGATED_ON_DATE, lastAggregatedOnDate);
|
||||
jobExecution.getExecutionContext().put(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_FROM, lastAggregatedOnDate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterJob(final JobExecution jobExecution) {
|
||||
// if job not completed successfully, rollback any insertion made.
|
||||
if (!successJobStatus.contains(jobExecution.getExitStatus().getExitCode())) {
|
||||
journalEntryAggregationWriterService.rollbackJournalEntrySummary(jobExecution.getId());
|
||||
journalEntryAggregationWriterService.rollbackJournalEntryTracking(jobExecution.getId());
|
||||
}
|
||||
// if job completed successfully, log the summary.
|
||||
logJobExecutionSummary(jobExecution);
|
||||
}
|
||||
|
||||
private void logJobExecutionSummary(final JobExecution jobExecution) {
|
||||
final LocalDate aggregatedOnDateFrom = (LocalDate) jobExecution.getExecutionContext()
|
||||
.get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_FROM);
|
||||
final LocalDate aggregatedOnDateTo = (LocalDate) jobExecution.getExecutionContext()
|
||||
.get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_TO);
|
||||
final Long jobExecutionId = jobExecution.getId();
|
||||
final Long recordProcessCount = jobExecution.getStepExecutions().stream()
|
||||
.filter(stepExecution -> stepExecution.getStepName().equals(JOB_SUMMARY_STEP_NAME)).mapToLong(StepExecution::getWriteCount)
|
||||
.sum();
|
||||
final Instant startDateTime = jobExecution.getStartTime() != null ? jobExecution.getStartTime().toInstant(ZoneOffset.UTC) : null;
|
||||
final Instant endDateTime = jobExecution.getEndTime() != null ? jobExecution.getEndTime().toInstant(ZoneOffset.UTC) : null;
|
||||
long jobDuration = 0L;
|
||||
Long startDateTimeMilliSecond = null;
|
||||
Long endDateTimeMilliSecond = null;
|
||||
if (startDateTime != null && endDateTime != null) {
|
||||
startDateTimeMilliSecond = startDateTime.toEpochMilli();
|
||||
endDateTimeMilliSecond = endDateTime.toEpochMilli();
|
||||
jobDuration = startDateTime.until(endDateTime, ChronoUnit.MINUTES);
|
||||
}
|
||||
log.info(
|
||||
"Execution Summary for jobName={}, aggregatedDateFrom={}, aggregatedDateTo={}, totalRecordProcessCount={}, startTime={}, endTime={}, startTime_ms={}, endTime_ms={}, "
|
||||
+ "jobExecutionId={}, jobExecutionDurationInMinutes={}, tenantId={}",
|
||||
JOURNAL_ENTRY_AGGREGATION_JOB_NAME, aggregatedOnDateFrom, aggregatedOnDateTo, recordProcessCount, startDateTime,
|
||||
endDateTime, startDateTimeMilliSecond, endDateTimeMilliSecond, jobExecutionId, jobDuration,
|
||||
ThreadLocalContextUtil.getTenant().getTenantIdentifier());
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 java.util.List;
|
||||
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.data.JournalEntryAggregationSummaryData;
|
||||
import org.apache.fineract.infrastructure.jobs.service.aggregationjob.data.JournalEntryAggregationTrackingData;
|
||||
|
||||
public interface JournalEntryAggregationWriterService {
|
||||
|
||||
void insertJournalEntrySummaryBatch(List<JournalEntryAggregationSummaryData> journalEntrySummaries);
|
||||
|
||||
void insertJournalEntryTracking(JournalEntryAggregationTrackingData journalEntrySummaryTrackingDTO) throws Exception;
|
||||
|
||||
void rollbackJournalEntrySummary(Long jobExecutionId);
|
||||
|
||||
void rollbackJournalEntryTracking(Long jobExecutionId);
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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 java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.JournalEntryAggregationTracking;
|
||||
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.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class JournalEntryAggregationWriterServiceImpl implements JournalEntryAggregationWriterService {
|
||||
|
||||
private JournalEntrySummaryRepository journalSummaryRepository;
|
||||
private JournalEntryAggregationTrackingRepository journalEntryAggregationTrackingRepository;
|
||||
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
|
||||
|
||||
@Override
|
||||
public void insertJournalEntrySummaryBatch(final List<JournalEntryAggregationSummaryData> journalEntrySummaries) {
|
||||
List<JournalEntrySummary> entities = journalEntrySummaries.stream().map(this::convertToJournalEntrySummary).toList();
|
||||
journalSummaryRepository.saveAll(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertJournalEntryTracking(JournalEntryAggregationTrackingData journalEntrySummaryTrackingDTO) throws Exception {
|
||||
log.info("Inserting journal entry tracking");
|
||||
saveJournalEntryTracking(journalEntrySummaryTrackingDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollbackJournalEntrySummary(final Long jobExecutionId) {
|
||||
log.info("Rolling back journal entry summary for jobExecutionId={}", jobExecutionId);
|
||||
journalSummaryRepository.deleteByJobExecutionId(jobExecutionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollbackJournalEntryTracking(final Long jobExecutionId) {
|
||||
log.info("Rolling back journal entry summary tracking for jobExecutionId={}", jobExecutionId);
|
||||
journalEntryAggregationTrackingRepository.deleteByJobExecutionId(jobExecutionId);
|
||||
}
|
||||
|
||||
private void saveJournalEntryTracking(final JournalEntryAggregationTrackingData journalEntrySummaryTrackingDTO) {
|
||||
JournalEntryAggregationTracking journalEntryAggregationTracking = new JournalEntryAggregationTracking();
|
||||
journalEntryAggregationTracking.setSubmittedOnDate(journalEntrySummaryTrackingDTO.getSubmittedOnDate());
|
||||
journalEntryAggregationTracking.setAggregatedOnDateFrom(journalEntrySummaryTrackingDTO.getAggregatedOnDateFrom());
|
||||
journalEntryAggregationTracking.setAggregatedOnDateTo(journalEntrySummaryTrackingDTO.getAggregatedOnDateTo());
|
||||
journalEntryAggregationTracking.setJobExecutionId(journalEntrySummaryTrackingDTO.getJobExecutionId());
|
||||
journalEntryAggregationTrackingRepository.save(journalEntryAggregationTracking);
|
||||
}
|
||||
|
||||
private JournalEntrySummary convertToJournalEntrySummary(final JournalEntryAggregationSummaryData summaryDTO) {
|
||||
JournalEntrySummary entrySummary = new JournalEntrySummary();
|
||||
entrySummary.setProduct(summaryDTO.getProductId());
|
||||
entrySummary.setGlAccountId(summaryDTO.getGlAccountId());
|
||||
entrySummary.setOffice(summaryDTO.getOffice());
|
||||
entrySummary.setEntityTypeEnum(summaryDTO.getEntityTypeEnum());
|
||||
entrySummary.setSubmittedOnDate(summaryDTO.getSubmittedOnDate());
|
||||
entrySummary.setDebitAmount(summaryDTO.getDebitAmount());
|
||||
entrySummary.setCreditAmount(summaryDTO.getCreditAmount());
|
||||
entrySummary.setExternalOwnerId(summaryDTO.getExternalOwnerId());
|
||||
entrySummary.setAggregatedOnDate(summaryDTO.getAggregatedOnDate());
|
||||
entrySummary.setJobExecutionId(summaryDTO.getJobExecutionId());
|
||||
return entrySummary;
|
||||
}
|
||||
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.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.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Slf4j
|
||||
public class JournalEntryAggregationTrackingTasklet implements Tasklet {
|
||||
|
||||
private final JournalEntryAggregationWriterService journalEntryAggregationWriterService;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, @NonNull ChunkContext chunkContext) throws Exception {
|
||||
// If no record is persisted in summary table then skip persisting data in tracking as well
|
||||
Optional<StepExecution> jobSummaryStepExecution = contribution.getStepExecution().getJobExecution().getStepExecutions().stream()
|
||||
.filter(stepExecution -> stepExecution.getStepName().equals(JournalEntryAggregationJobConstant.JOB_SUMMARY_STEP_NAME))
|
||||
.findFirst();
|
||||
long writeCount = jobSummaryStepExecution.map(StepExecution::getWriteCount).orElse(0L);
|
||||
|
||||
log.info("Starting journal entry aggregation tasklet to insert into tracking table writeCount={}", writeCount);
|
||||
if (writeCount > 0) {
|
||||
final JobExecution jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution();
|
||||
final LocalDate aggregatedOnDateFrom = (LocalDate) jobExecutionContext.getExecutionContext()
|
||||
.get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_FROM);
|
||||
final LocalDate aggregatedOnDateTo = (LocalDate) jobExecutionContext.getExecutionContext()
|
||||
.get(JournalEntryAggregationJobConstant.AGGREGATED_ON_DATE_TO);
|
||||
JournalEntryAggregationTrackingData journalEntrySummaryTrackingDTO = JournalEntryAggregationTrackingData.builder()
|
||||
.submittedOnDate(ThreadLocalContextUtil.getBusinessDate()).aggregatedOnDateFrom(aggregatedOnDateFrom)
|
||||
.aggregatedOnDateTo(aggregatedOnDateTo).jobExecutionId(contribution.getStepExecution().getJobExecution().getId())
|
||||
.build();
|
||||
journalEntryAggregationWriterService.insertJournalEntryTracking(journalEntrySummaryTrackingDTO);
|
||||
}
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,11 @@ fineract.correlation.header-name=${FINERACT_LOGGING_HTTP_CORRELATION_ID_HEADER_N
|
||||
fineract.job.stuck-retry-threshold=${FINERACT_JOB_STUCK_RETRY_THRESHOLD:5}
|
||||
fineract.ip-tracking.enabled=${FINERACT_CLIENT_IP_TRACKING_ENABLED:false}
|
||||
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.partitioned-job.partitioned-job-properties[0].job-name=LOAN_COB
|
||||
fineract.partitioned-job.partitioned-job-properties[0].chunk-size=${LOAN_COB_CHUNK_SIZE:100}
|
||||
|
||||
@@ -218,4 +218,7 @@
|
||||
<include file="parts/0197_add_deleted_and_closed_to_capitalized_income_balance.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0198_add_classification_id_to_acc_product_mapping.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0199_write_off_reason_mapping_loan.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0200_add_journal_entry_aggregation_tables.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0201_add_journal_entry_aggregation_job.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0202_trial_balance_summary_with_asset_owner_journal_entry_aggregation.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
<?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 id="1" author="fineract" context="postgresql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_journal_entry_aggregation_summary"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_journal_entry_aggregation_summary">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_journal_entry_aggregation_summary"/>
|
||||
</column>
|
||||
<column name="gl_account_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="product_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="office_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="entity_type_enum" type="SMALLINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="aggregated_on_date" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="submitted_on_date" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="external_owner_id" type="BIGINT"/>
|
||||
<column name="debit_amount" type="DECIMAL(19, 6)"/>
|
||||
<column name="credit_amount" type="DECIMAL(19, 6)"/>
|
||||
<column defaultValueBoolean="false" name="manual_entry" type="boolean">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="job_execution_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="BIGINT"/>
|
||||
<column name="created_on_utc" type="timestamp with time zone"/>
|
||||
<column name="last_modified_by" type="BIGINT"/>
|
||||
<column name="last_modified_on_utc" type="timestamp with time zone"/>
|
||||
</createTable>
|
||||
<addForeignKeyConstraint baseColumnNames="created_by" baseTableName="m_journal_entry_aggregation_summary" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_SUMMARY_ON_CREATED_BY" referencedColumnNames="id" referencedTableName="m_appuser"/>
|
||||
<addForeignKeyConstraint baseColumnNames="last_modified_by" baseTableName="m_journal_entry_aggregation_summary" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_SUMMARY_ON_LAST_MODIFIED_BY" referencedColumnNames="id" referencedTableName="m_appuser"/>
|
||||
<addForeignKeyConstraint baseColumnNames="office_id" baseTableName="m_journal_entry_aggregation_summary" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_SUMMARY_ON_OFFICE_ID" referencedColumnNames="id" referencedTableName="m_office"/>
|
||||
<addForeignKeyConstraint baseColumnNames="gl_account_id" baseTableName="m_journal_entry_aggregation_summary" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_SUMMARY_ON_GL_ACCOUNT_ID" referencedColumnNames="id" referencedTableName="acc_gl_account"/>
|
||||
<addForeignKeyConstraint baseColumnNames="job_execution_id" baseTableName="m_journal_entry_aggregation_summary" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_SUMMARY_ON_JOB_EXECUTION_ID" referencedColumnNames="job_execution_id" referencedTableName="batch_job_execution"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="2" author="fineract" runInTransaction="false" context="postgresql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<indexExists tableName="m_journal_entry_aggregation_summary" columnNames="submitted_on_date,gl_account_id"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<sql>
|
||||
create index concurrently idx_journal_entry_aggregation_summary on m_journal_entry_aggregation_summary(submitted_on_date,gl_account_id);
|
||||
</sql>
|
||||
</changeSet>
|
||||
<changeSet id="3" author="fineract" context="postgresql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_journal_entry_aggregation_tracking"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_journal_entry_aggregation_tracking">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_journal_entry_aggregation_tracking"/>
|
||||
</column>
|
||||
<column name="aggregated_on_date_from" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="aggregated_on_date_to" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="submitted_on_date" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="job_execution_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="BIGINT"/>
|
||||
<column name="created_on_utc" type="timestamp with time zone"/>
|
||||
<column name="last_modified_by" type="BIGINT"/>
|
||||
<column name="last_modified_on_utc" type="timestamp with time zone"/>
|
||||
</createTable>
|
||||
<addForeignKeyConstraint baseColumnNames="job_execution_id" baseTableName="m_journal_entry_aggregation_tracking" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_TRACKING_ON_JOB_EXECUTION_ID" referencedColumnNames="job_execution_id" referencedTableName="batch_job_execution"/>
|
||||
<addForeignKeyConstraint baseColumnNames="created_by" baseTableName="m_journal_entry_aggregation_tracking" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_TRACKING_ON_CREATED_BY" referencedColumnNames="id" referencedTableName="m_appuser"/>
|
||||
<addForeignKeyConstraint baseColumnNames="last_modified_by" baseTableName="m_journal_entry_aggregation_tracking" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_TRACKING_ON_LAST_MODIFIED_BY" referencedColumnNames="id" referencedTableName="m_appuser"/>
|
||||
</changeSet>
|
||||
<changeSet id="4" author="fineract" runInTransaction="false" context="postgresql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<indexExists tableName="m_journal_entry_aggregation_tracking" columnNames="submitted_on_date"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<sql>
|
||||
create index concurrently idx_m_journal_entry_aggregation_tracking on m_journal_entry_aggregation_tracking(submitted_on_date);
|
||||
</sql>
|
||||
</changeSet>
|
||||
<changeSet id="5" author="fineract" runInTransaction="false" context="postgresql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<indexExists tableName="m_journal_entry_aggregation_tracking" columnNames="aggregated_on_date_to"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<sql>
|
||||
create unique index concurrently idx2_m_journal_entry_aggregation_tracking on m_journal_entry_aggregation_tracking(aggregated_on_date_to);
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="6" author="fineract" context="mysql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_journal_entry_aggregation_summary"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_journal_entry_aggregation_summary">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_journal_entry_aggregation_summary"/>
|
||||
</column>
|
||||
<column name="gl_account_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="product_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="office_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="entity_type_enum" type="SMALLINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="aggregated_on_date" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="submitted_on_date" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="external_owner_id" type="BIGINT"/>
|
||||
<column name="debit_amount" type="DECIMAL(19, 6)"/>
|
||||
<column name="credit_amount" type="DECIMAL(19, 6)"/>
|
||||
<column defaultValueBoolean="false" name="manual_entry" type="boolean">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="job_execution_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="BIGINT"/>
|
||||
<column name="created_on_utc" type="DATETIME"/>
|
||||
<column name="last_modified_by" type="BIGINT"/>
|
||||
<column name="last_modified_on_utc" type="DATETIME"/>
|
||||
</createTable>
|
||||
<addForeignKeyConstraint baseColumnNames="created_by" baseTableName="m_journal_entry_aggregation_summary" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_SUMMARY_ON_CREATED_BY" referencedColumnNames="id" referencedTableName="m_appuser"/>
|
||||
<addForeignKeyConstraint baseColumnNames="last_modified_by" baseTableName="m_journal_entry_aggregation_summary" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_SUMMARY_ON_LAST_MODIFIED_BY" referencedColumnNames="id" referencedTableName="m_appuser"/>
|
||||
<addForeignKeyConstraint baseColumnNames="office_id" baseTableName="m_journal_entry_aggregation_summary" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_SUMMARY_ON_OFFICE_ID" referencedColumnNames="id" referencedTableName="m_office"/>
|
||||
<addForeignKeyConstraint baseColumnNames="gl_account_id" baseTableName="m_journal_entry_aggregation_summary" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_SUMMARY_ON_GL_ACCOUNT_ID" referencedColumnNames="id" referencedTableName="acc_gl_account"/>
|
||||
<addForeignKeyConstraint baseColumnNames="job_execution_id" baseTableName="m_journal_entry_aggregation_summary" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_SUMMARY_ON_JOB_EXECUTION_ID" referencedColumnNames="JOB_EXECUTION_ID" referencedTableName="BATCH_JOB_EXECUTION"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="7" author="fineract" runInTransaction="false" context="mysql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<indexExists tableName="m_journal_entry_aggregation_summary" columnNames="submitted_on_date,gl_account_id"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<sql>
|
||||
create index idx_journal_entry_aggregation_summary on m_journal_entry_aggregation_summary(submitted_on_date,gl_account_id);
|
||||
</sql>
|
||||
</changeSet>
|
||||
<changeSet id="8" author="fineract" context="mysql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<tableExists tableName="m_journal_entry_aggregation_tracking"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_journal_entry_aggregation_tracking">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_journal_entry_aggregation_tracking"/>
|
||||
</column>
|
||||
<column name="aggregated_on_date_from" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="aggregated_on_date_to" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="submitted_on_date" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="job_execution_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_by" type="BIGINT"/>
|
||||
<column name="created_on_utc" type="DATETIME"/>
|
||||
<column name="last_modified_by" type="BIGINT"/>
|
||||
<column name="last_modified_on_utc" type="DATETIME"/>
|
||||
</createTable>
|
||||
<addForeignKeyConstraint baseColumnNames="job_execution_id" baseTableName="m_journal_entry_aggregation_tracking" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_TRACKING_ON_JOB_EXECUTION_ID" referencedColumnNames="JOB_EXECUTION_ID" referencedTableName="BATCH_JOB_EXECUTION"/>
|
||||
<addForeignKeyConstraint baseColumnNames="created_by" baseTableName="m_journal_entry_aggregation_tracking" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_TRACKING_ON_CREATED_BY" referencedColumnNames="id" referencedTableName="m_appuser"/>
|
||||
<addForeignKeyConstraint baseColumnNames="last_modified_by" baseTableName="m_journal_entry_aggregation_tracking" constraintName="FK_GL_JOURNAL_ENTRY_AGGREGATION_TRACKING_ON_LAST_MODIFIED_BY" referencedColumnNames="id" referencedTableName="m_appuser"/>
|
||||
</changeSet>
|
||||
<changeSet id="9" author="fineract" runInTransaction="false" context="mysql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<indexExists tableName="m_journal_entry_aggregation_tracking" columnNames="submitted_on_date"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<sql>
|
||||
create index idx_m_journal_entry_aggregation_tracking on m_journal_entry_aggregation_tracking(submitted_on_date);
|
||||
</sql>
|
||||
</changeSet>
|
||||
<changeSet id="10" author="fineract" runInTransaction="false" context="mysql">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not>
|
||||
<indexExists tableName="m_journal_entry_aggregation_tracking" columnNames="aggregated_on_date_to"/>
|
||||
</not>
|
||||
</preConditions>
|
||||
<sql>
|
||||
create unique index idx2_m_journal_entry_aggregation_tracking on m_journal_entry_aggregation_tracking(aggregated_on_date_to);
|
||||
</sql>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
<property name="current_datetime" value="NOW()"/>
|
||||
<changeSet id="1" author="fineract">
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<sqlCheck expectedResult="0">select count(1) from job where short_name = 'JRNL_ENTRY_AGG'</sqlCheck>
|
||||
</preConditions>
|
||||
<insert tableName="job">
|
||||
<column name="name" value="Journal Entry Aggregation"/>
|
||||
<column name="display_name" value="Journal Entry Aggregation"/>
|
||||
<column name="cron_expression" value="0 0 6 * * ?"/>
|
||||
<column name="create_time" valueDate="${current_datetime}"/>
|
||||
<column name="task_priority" valueNumeric="5"/>
|
||||
<column name="group_name"/>
|
||||
<column name="previous_run_start_time"/>
|
||||
<column name="job_key" value="Journal Entry Aggregation _ DEFAULT"/>
|
||||
<column name="initializing_errorlog"/>
|
||||
<column name="is_active" valueBoolean="false"/>
|
||||
<column name="currently_running" valueBoolean="false"/>
|
||||
<column name="updates_allowed" valueBoolean="true"/>
|
||||
<column name="scheduler_group" valueNumeric="0"/>
|
||||
<column name="is_misfired" valueBoolean="false"/>
|
||||
<column name="node_id" valueNumeric="1"/>
|
||||
<column name="is_mismatched_job" valueBoolean="true"/>
|
||||
<column name="short_name" value="JRNL_AGG"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
|
||||
<changeSet author="fineract" id="trial-balance-summary-with-asset-owner-update-1">
|
||||
<update tableName="stretchy_report">
|
||||
<column name="report_sql" value="WITH retained_earning AS (
|
||||
SELECT DISTINCT '${endDate}' AS postingdate,
|
||||
lp.name AS product,
|
||||
gl_code AS glacct,
|
||||
COALESCE((SELECT name FROM acc_gl_account WHERE gl_code = e.gl_code), '') AS description,
|
||||
COALESCE(e.owner_external_id, 'self') AS assetowner,
|
||||
SUM(opening_balance_amount) AS beginningbalance,
|
||||
0 AS debitmovement,
|
||||
0 AS creditmovement,
|
||||
SUM(opening_balance_amount) AS endingbalance
|
||||
FROM acc_gl_journal_entry_annual_summary e, m_product_loan lp
|
||||
WHERE e.office_id = ${officeId}
|
||||
AND lp.id = product_id
|
||||
AND date_part('year', e.year_end_date::date) < date_part('year', '${endDate}'::date)
|
||||
GROUP BY gl_code, lp.name, office_id, owner_external_id
|
||||
),
|
||||
aggregated_date AS (
|
||||
SELECT MAX(aggregated_on_date_to) as latest
|
||||
FROM m_journal_entry_aggregation_tracking
|
||||
),
|
||||
summary_snapshot_baseline_data AS (
|
||||
--using m_journal_entry_aggregation_summary to fetch aggregated journal entry data
|
||||
SELECT lp.NAME AS productname,
|
||||
acc_gl_account.gl_code AS glcode,
|
||||
acc_gl_account.NAME AS glname,
|
||||
CASE WHEN ags.external_owner_id IS NULL THEN 0 ELSE ags.external_owner_id END AS assetowner,
|
||||
SUM(ags.debit_amount) AS debitamount,
|
||||
SUM(ags.credit_amount) AS creditamount
|
||||
FROM acc_gl_account
|
||||
JOIN m_journal_entry_aggregation_summary ags ON acc_gl_account.id = ags.gl_account_id
|
||||
JOIN m_product_loan lp ON lp.id = ags.product_id
|
||||
WHERE ags.entity_type_enum = 1
|
||||
AND ags.manual_entry = false
|
||||
AND ags.submitted_on_date <= (select latest from aggregated_date)
|
||||
AND (ags.office_id = ${officeId})
|
||||
GROUP BY productname, glcode, glname, assetowner
|
||||
),
|
||||
post_snapshot_delta_data AS (
|
||||
-- still using acc_gl_journal_entry for dates after last aggregated data and before the cob date
|
||||
SELECT lp.NAME AS productname,
|
||||
acc_gl_account.gl_code AS glcode,
|
||||
acc_gl_account.NAME AS glname,
|
||||
CASE WHEN aw.owner_id IS NULL THEN 0 ELSE aw.owner_id END AS assetowner,
|
||||
SUM(CASE WHEN acc_gl_journal_entry.type_enum = 2 THEN amount ELSE 0 END) AS debitamount,
|
||||
SUM(CASE WHEN acc_gl_journal_entry.type_enum = 1 THEN amount ELSE 0 END) AS creditamount
|
||||
FROM acc_gl_account
|
||||
JOIN acc_gl_journal_entry ON acc_gl_account.id = acc_gl_journal_entry.account_id
|
||||
JOIN m_loan m ON m.id = acc_gl_journal_entry.entity_id
|
||||
JOIN m_product_loan lp ON lp.id = m.product_id
|
||||
LEFT JOIN m_external_asset_owner_journal_entry_mapping aw ON aw.journal_entry_id = acc_gl_journal_entry.id
|
||||
WHERE acc_gl_journal_entry.entity_type_enum = 1
|
||||
AND acc_gl_journal_entry.manual_entry = false
|
||||
AND acc_gl_journal_entry.submitted_on_date > (select latest from aggregated_date)
|
||||
AND acc_gl_journal_entry.submitted_on_date < '${endDate}'
|
||||
AND (acc_gl_journal_entry.office_id = ${officeId})
|
||||
GROUP BY productname, glcode, glname, assetowner
|
||||
),
|
||||
-- fetch all data before cob date
|
||||
merged_historical_data AS (
|
||||
SELECT summary_snapshot_baseline_data.productname,
|
||||
summary_snapshot_baseline_data.glcode,
|
||||
summary_snapshot_baseline_data.glname,
|
||||
summary_snapshot_baseline_data.assetowner,
|
||||
COALESCE(summary_snapshot_baseline_data.debitamount, 0) + COALESCE(post_snapshot_delta_data.debitamount, 0) AS debitamount,
|
||||
COALESCE(summary_snapshot_baseline_data.creditamount, 0) + COALESCE(post_snapshot_delta_data.creditamount, 0) AS creditamount
|
||||
FROM summary_snapshot_baseline_data
|
||||
FULL OUTER JOIN post_snapshot_delta_data ON summary_snapshot_baseline_data.glcode = post_snapshot_delta_data.glcode
|
||||
AND summary_snapshot_baseline_data.productname = post_snapshot_delta_data.productname
|
||||
AND summary_snapshot_baseline_data.assetowner = post_snapshot_delta_data.assetowner
|
||||
),
|
||||
-- fetch data for cob date
|
||||
current_cob_data AS (
|
||||
SELECT lp.name AS productname,
|
||||
account_id,
|
||||
acc_gl_account.gl_code AS glcode,
|
||||
acc_gl_account.name AS glname,
|
||||
CASE WHEN aw.owner_id IS NULL THEN 0 ELSE aw.owner_id END AS assetowner,
|
||||
SUM(CASE WHEN acc_gl_journal_entry.type_enum = 2 THEN amount ELSE 0 END) AS debitamount,
|
||||
SUM(CASE WHEN acc_gl_journal_entry.type_enum = 1 THEN amount ELSE 0 END) AS creditamount
|
||||
FROM acc_gl_journal_entry
|
||||
JOIN acc_gl_account ON acc_gl_account.id = acc_gl_journal_entry.account_id
|
||||
JOIN m_loan m ON m.id = acc_gl_journal_entry.entity_id
|
||||
JOIN m_product_loan lp ON lp.id = m.product_id
|
||||
LEFT JOIN m_external_asset_owner_journal_entry_mapping aw ON aw.journal_entry_id = acc_gl_journal_entry.id
|
||||
WHERE acc_gl_journal_entry.entity_type_enum = 1
|
||||
AND acc_gl_journal_entry.manual_entry = false
|
||||
AND acc_gl_journal_entry.submitted_on_date = '${endDate}'
|
||||
AND (acc_gl_journal_entry.office_id = ${officeId})
|
||||
GROUP BY productname, account_id, glcode, glname, assetowner
|
||||
)
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM retained_earning
|
||||
WHERE glacct = (SELECT gl_code FROM acc_gl_account WHERE name = 'Retained Earnings Prior Year')
|
||||
|
||||
UNION
|
||||
|
||||
SELECT txnreport.postingdate,
|
||||
txnreport.product,
|
||||
txnreport.glacct,
|
||||
txnreport.description,
|
||||
txnreport.assetowner,
|
||||
(COALESCE(txnreport.beginningbalance, 0) + COALESCE(summary.beginningbalance, 0)) AS beginningbalance,
|
||||
txnreport.debitmovement AS debitmovement,
|
||||
txnreport.creditmovement AS creditmovement,
|
||||
(COALESCE(txnreport.endingbalance, 0) + COALESCE(summary.beginningbalance, 0)) AS endingbalance
|
||||
FROM (
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT DISTINCT '${endDate}' AS postingdate,
|
||||
loan.pname AS product,
|
||||
loan.gl_code AS glacct,
|
||||
loan.glname AS description,
|
||||
COALESCE((SELECT external_id FROM m_external_asset_owner WHERE id = loan.assetowner), 'self') AS
|
||||
assetowner,
|
||||
loan.openingbalance AS beginningbalance,
|
||||
(loan.debitamount * 1) AS debitmovement,
|
||||
(loan.creditamount * -1) AS creditmovement,
|
||||
(loan.openingbalance + loan.debitamount - loan.creditamount) AS endingbalance
|
||||
FROM (
|
||||
SELECT DISTINCT COALESCE(g.pname, current_cob_data.productname) pname,
|
||||
COALESCE(g.gl_code, current_cob_data.glcode) gl_code,
|
||||
COALESCE(g.glname, current_cob_data.glname) glname,
|
||||
COALESCE(merged_historical_data.assetowner, current_cob_data.assetowner) assetowner,
|
||||
COALESCE(merged_historical_data.debitamount, 0) - COALESCE(merged_historical_data.creditamount, 0) AS openingbalance,
|
||||
COALESCE(current_cob_data.debitamount, 0) AS debitamount,
|
||||
COALESCE(current_cob_data.creditamount, 0) AS creditamount
|
||||
FROM (
|
||||
SELECT DISTINCT ag.gl_code, ag.id, pl.NAME AS pname, ag.NAME AS glname
|
||||
FROM acc_gl_account ag
|
||||
JOIN acc_product_mapping am ON am.gl_account_id = ag.id AND am.product_type = 1
|
||||
JOIN m_product_loan pl ON pl.id = am.product_id
|
||||
) g
|
||||
LEFT JOIN merged_historical_data ON g.gl_code = merged_historical_data.glcode AND merged_historical_data.productname = g.pname
|
||||
FULL OUTER JOIN current_cob_data ON g.gl_code = current_cob_data.glcode
|
||||
AND current_cob_data.productname = g.pname
|
||||
AND merged_historical_data.assetowner = current_cob_data.assetowner
|
||||
) loan
|
||||
) a
|
||||
) AS txnreport
|
||||
LEFT JOIN retained_earning summary ON txnreport.glacct = summary.glacct
|
||||
AND txnreport.assetowner = summary.assetowner
|
||||
AND summary.product = txnreport.product
|
||||
) report
|
||||
WHERE report.endingbalance != 0
|
||||
OR report.debitmovement != 0
|
||||
OR report.creditmovement != 0
|
||||
ORDER BY glacct"/>
|
||||
<where>report_name='Trial Balance Summary Report with Asset Owner'</where>
|
||||
</update>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+92
@@ -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");
|
||||
}
|
||||
}
|
||||
+133
@@ -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");
|
||||
}
|
||||
}
|
||||
+151
@@ -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);
|
||||
}
|
||||
}
|
||||
+110
@@ -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);
|
||||
}
|
||||
}
|
||||
+131
@@ -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));
|
||||
}
|
||||
}
|
||||
+153
@@ -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();
|
||||
}
|
||||
}
|
||||
+136
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user