[FINERACT-1667] Introducing Spring Batch as job runner engine

This commit is contained in:
taskain7
2022-07-28 12:45:01 +02:00
committed by Arnold Gálovics
parent d03044c48b
commit ff9181cc47
132 changed files with 5995 additions and 3228 deletions
+7
View File
@@ -66,3 +66,10 @@ jobs:
- name: OAuth2 Build & Test
run: ./gradlew --no-daemon -q --console=plain :oauth2-tests:test --fail-fast -PdbType=postgresql
- name: Archive test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: home/runner/work/fineract/fineract/integration-tests/build/reports/
+3
View File
@@ -129,6 +129,9 @@ dependencies {
exclude group: 'xml-apis'
}
implementation ('org.springframework.boot:spring-boot-starter-batch') {
}
runtimeOnly('org.glassfish.jaxb:jaxb-runtime') {
exclude group: 'com.sun.activation'
}
@@ -0,0 +1,60 @@
/**
* 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.accounting.glaccount.jobs.updatetrialbalancedetails;
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepositoryWrapper;
import org.apache.fineract.infrastructure.core.service.RoutingDataSourceServiceFactory;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UpdateTrialBalanceDetailsConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private RoutingDataSourceServiceFactory dataSourceServiceFactory;
@Autowired
private TrialBalanceRepositoryWrapper trialBalanceRepositoryWrapper;
@Bean
protected Step updateTrialBalanceDetailsStep() {
return steps.get(JobName.UPDATE_TRIAL_BALANCE_DETAILS.name()).tasklet(updateTrialBalanceDetailsTasklet()).build();
}
@Bean
public Job updateTrialBalanceDetailsJob() {
return jobs.get(JobName.UPDATE_TRIAL_BALANCE_DETAILS.name()).start(updateTrialBalanceDetailsStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public UpdateTrialBalanceDetailsTasklet updateTrialBalanceDetailsTasklet() {
return new UpdateTrialBalanceDetailsTasklet(dataSourceServiceFactory, trialBalanceRepositoryWrapper);
}
}
@@ -0,0 +1,100 @@
/**
* 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.accounting.glaccount.jobs.updatetrialbalancedetails;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.glaccount.domain.TrialBalance;
import org.apache.fineract.accounting.glaccount.domain.TrialBalanceRepositoryWrapper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.RoutingDataSourceServiceFactory;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.springframework.batch.core.StepContribution;
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.jdbc.core.JdbcTemplate;
import org.springframework.util.CollectionUtils;
@Slf4j
@RequiredArgsConstructor
public class UpdateTrialBalanceDetailsTasklet implements Tasklet {
private final RoutingDataSourceServiceFactory dataSourceServiceFactory;
private final TrialBalanceRepositoryWrapper trialBalanceRepositoryWrapper;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource());
final StringBuilder tbGapSqlBuilder = new StringBuilder(500);
tbGapSqlBuilder.append("select distinct(je.transaction_date) ").append("from acc_gl_journal_entry je ")
.append("where je.transaction_date > (select coalesce(MAX(created_date),'2010-01-01') from m_trial_balance)");
final List<LocalDate> tbGaps = jdbcTemplate.queryForList(tbGapSqlBuilder.toString(), LocalDate.class);
for (LocalDate tbGap : tbGaps) {
int days = Math.toIntExact(ChronoUnit.DAYS.between(tbGap, DateUtils.getBusinessLocalDate()));
if (days < 1) {
continue;
}
final StringBuilder sqlBuilder = new StringBuilder(600);
sqlBuilder.append("Insert Into m_trial_balance(office_id, account_id, Amount, entry_date, created_date,closing_balance) ")
.append("Select je.office_id, je.account_id, SUM(CASE WHEN je.type_enum=1 THEN (-1) * je.amount ELSE je.amount END) ")
.append("as Amount, Date(je.entry_date) as Entry_Date, je.transaction_date as Created_Date,sum(je.amount) as closing_balance ")
.append("from acc_gl_journal_entry je WHERE je.transaction_date = ? ")
.append("group by je.account_id, je.office_id, je.transaction_date, Date(je.entry_date)");
final int result = jdbcTemplate.update(sqlBuilder.toString(), tbGap);
log.info("{}: Records affected by updateTrialBalanceDetails: {}", ThreadLocalContextUtil.getTenant().getName(), result);
}
String distinctOfficeQuery = "select distinct(office_id) from m_trial_balance where closing_balance is null group by office_id";
final List<Long> officeIds = jdbcTemplate.queryForList(distinctOfficeQuery, Long.class);
for (Long officeId : officeIds) {
String distinctAccountQuery = "select distinct(account_id) from m_trial_balance where office_id=? and closing_balance is null group by account_id";
final List<Long> accountIds = jdbcTemplate.queryForList(distinctAccountQuery, Long.class, officeId);
for (Long accountId : accountIds) {
final String closingBalanceQuery = "select closing_balance from m_trial_balance where office_id=? and account_id=? and closing_balance "
+ "is not null order by created_date desc, entry_date desc limit 1";
List<BigDecimal> closingBalanceData = jdbcTemplate.queryForList(closingBalanceQuery, BigDecimal.class, officeId, accountId);
List<TrialBalance> tbRows = trialBalanceRepositoryWrapper.findNewByOfficeAndAccount(officeId, accountId);
BigDecimal closingBalance = null;
if (!CollectionUtils.isEmpty(closingBalanceData)) {
closingBalance = closingBalanceData.get(0);
}
if (CollectionUtils.isEmpty(closingBalanceData)) {
closingBalance = BigDecimal.ZERO;
for (TrialBalance row : tbRows) {
closingBalance = closingBalance.add(row.getAmount());
row.setClosingBalance(closingBalance);
}
} else {
for (TrialBalance tbRow : tbRows) {
if (closingBalance != null) {
closingBalance = closingBalance.add(tbRow.getAmount());
}
tbRow.setClosingBalance(closingBalance);
}
}
trialBalanceRepositoryWrapper.save(tbRows);
}
}
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,58 @@
/**
* 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.accounting.jobs.accountrunningbalanceupdate;
import org.apache.fineract.accounting.journalentry.service.JournalEntryRunningBalanceUpdateService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AccountRunningBalanceUpdateConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private JournalEntryRunningBalanceUpdateService journalEntryRunningBalanceUpdateService;
@Bean
protected Step accountRunningBalanceUpdateStep() {
return steps.get(JobName.ACCOUNTING_RUNNING_BALANCE_UPDATE.name()).tasklet(accountRunningBalanceUpdateTasklet()).build();
}
@Bean
public Job accountRunningBalanceUpdateJob() {
return jobs.get(JobName.ACCOUNTING_RUNNING_BALANCE_UPDATE.name()).start(accountRunningBalanceUpdateStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public AccountRunningBalanceUpdateTasklet accountRunningBalanceUpdateTasklet() {
return new AccountRunningBalanceUpdateTasklet(journalEntryRunningBalanceUpdateService);
}
}
@@ -0,0 +1,40 @@
/**
* 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.accounting.jobs.accountrunningbalanceupdate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.journalentry.service.JournalEntryRunningBalanceUpdateService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class AccountRunningBalanceUpdateTasklet implements Tasklet {
private final JournalEntryRunningBalanceUpdateService journalEntryRunningBalanceUpdateService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
journalEntryRunningBalanceUpdateService.updateRunningBalance();
return RepeatStatus.FINISHED;
}
}
@@ -41,8 +41,6 @@ import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -66,7 +64,6 @@ public class JournalEntryRunningBalanceUpdateServiceImpl implements JournalEntry
private final GLJournalEntryMapper entryMapper = new GLJournalEntryMapper();
@Override
@CronTarget(jobName = JobName.ACCOUNTING_RUNNING_BALANCE_UPDATE)
public void updateRunningBalance() {
String dateFinder = "select MIN(je.entry_date) as entityDate from acc_gl_journal_entry je "
+ "where je.is_running_balance_calculated=false ";
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.accounting.provisioning.service;
import java.time.LocalDate;
import org.apache.fineract.accounting.provisioning.domain.ProvisioningEntry;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
@@ -25,9 +27,9 @@ public interface ProvisioningEntriesWritePlatformService {
CommandProcessingResult createProvisioningEntries(JsonCommand command);
ProvisioningEntry createProvisioningEntry(LocalDate date, boolean addJournalEntries);
CommandProcessingResult reCreateProvisioningEntries(Long provisioningEntryId, JsonCommand command);
CommandProcessingResult createProvisioningJournalEntries(Long provisioningEntryId, JsonCommand command);
void generateLoanLossProvisioningAmount();
}
@@ -23,7 +23,6 @@ import java.time.LocalDate;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
@@ -44,9 +43,6 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -129,27 +125,6 @@ public class ProvisioningEntriesWritePlatformServiceJpaRepositoryImpl implements
return this.fromApiJsonHelper.extractLocalDateNamed("date", command.parsedJson());
}
@Override
@CronTarget(jobName = JobName.GENERATE_LOANLOSS_PROVISIONING)
public void generateLoanLossProvisioningAmount() {
LocalDate currentDate = DateUtils.getBusinessLocalDate();
boolean addJournalEntries = true;
try {
Collection<ProvisioningCriteriaData> criteriaCollection = this.provisioningCriteriaReadPlatformService
.retrieveAllProvisioningCriterias();
if (criteriaCollection == null || criteriaCollection.size() == 0) {
return;
// FIXME: Do we need to throw
// NoProvisioningCriteriaDefinitionFound()?
}
createProvsioningEntry(currentDate, addJournalEntries);
} catch (ProvisioningEntryAlreadyCreatedException peace) {
log.error("Provisioning Entry already created", peace);
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
log.error("Problem occurred in generateLoanLossProvisioningAmount function", dve);
}
}
@Override
public CommandProcessingResult createProvisioningEntries(JsonCommand command) {
this.fromApiJsonDeserializer.validateForCreate(command.json());
@@ -161,29 +136,27 @@ public class ProvisioningEntriesWritePlatformServiceJpaRepositoryImpl implements
if (criteriaCollection == null || criteriaCollection.size() == 0) {
throw new NoProvisioningCriteriaDefinitionFound();
}
ProvisioningEntry requestedEntry = createProvsioningEntry(createdDate, addJournalEntries);
ProvisioningEntry requestedEntry = createProvisioningEntry(createdDate, addJournalEntries);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(requestedEntry.getId()).build();
} catch (final JpaSystemException | DataIntegrityViolationException e) {
return CommandProcessingResult.empty();
}
}
private ProvisioningEntry createProvsioningEntry(LocalDate date, boolean addJournalEntries) {
@Override
public ProvisioningEntry createProvisioningEntry(LocalDate date, boolean addJournalEntries) {
ProvisioningEntry existingEntry = this.provisioningEntryRepository.findByProvisioningEntryDate(date);
if (existingEntry != null) {
throw new ProvisioningEntryAlreadyCreatedException(existingEntry.getId(), existingEntry.getCreatedDate());
}
AppUser currentUser = this.platformSecurityContext.authenticatedUser();
AppUser lastModifiedBy = null;
LocalDate lastModifiedDate = null;
Set<LoanProductProvisioningEntry> nullEntries = null;
ProvisioningEntry requestedEntry = new ProvisioningEntry(currentUser, date, lastModifiedBy, lastModifiedDate, nullEntries);
ProvisioningEntry requestedEntry = new ProvisioningEntry(currentUser, date, null, null, null);
Collection<LoanProductProvisioningEntry> entries = generateLoanProvisioningEntry(requestedEntry, date);
requestedEntry.setProvisioningEntries(entries);
if (addJournalEntries) {
ProvisioningEntryData exisProvisioningEntryData = this.provisioningEntriesReadPlatformService
ProvisioningEntryData existingProvisioningEntryData = this.provisioningEntriesReadPlatformService
.retrieveExistingProvisioningIdDateWithJournals();
revertAndAddJournalEntries(exisProvisioningEntryData, requestedEntry);
revertAndAddJournalEntries(existingProvisioningEntryData, requestedEntry);
} else {
this.provisioningEntryRepository.saveAndFlush(requestedEntry);
}
@@ -35,6 +35,7 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.adhocquery.data.AdHocData;
import org.apache.fineract.adhocquery.service.AdHocReadPlatformService;
import org.apache.fineract.commands.domain.CommandWrapper;
@@ -45,7 +46,6 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@@ -53,6 +53,7 @@ import org.springframework.stereotype.Component;
@Component
@Scope("singleton")
@Tag(name = "AdhocQuery Api", description = "")
@RequiredArgsConstructor
public class AdHocApiResource {
/**
@@ -67,17 +68,6 @@ public class AdHocApiResource {
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@Autowired
public AdHocApiResource(final PlatformSecurityContext context, final AdHocReadPlatformService readPlatformService,
final DefaultToApiJsonSerializer<AdHocData> toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper,
final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService) {
this.context = context;
this.adHocReadPlatformService = readPlatformService;
this.toApiJsonSerializer = toApiJsonSerializer;
this.apiRequestParameterHelper = apiRequestParameterHelper;
this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@@ -105,10 +95,7 @@ public class AdHocApiResource {
@Produces({ MediaType.APPLICATION_JSON })
public String createAdHocQuery(final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.createAdHoc() //
.withJson(apiRequestBodyAsJson) //
.build();
final CommandWrapper commandRequest = new CommandWrapperBuilder().createAdHoc().withJson(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
@@ -137,10 +124,7 @@ public class AdHocApiResource {
@Produces({ MediaType.APPLICATION_JSON })
public String update(@PathParam("adHocId") @Parameter(description = "adHocId") final Long adHocId, final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.updateAdHoc(adHocId) //
.withJson(apiRequestBodyAsJson) //
.build();
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateAdHoc(adHocId).withJson(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
@@ -159,9 +143,7 @@ public class AdHocApiResource {
@Produces({ MediaType.APPLICATION_JSON })
public String deleteAdHocQuery(@PathParam("adHocId") @Parameter(description = "adHocId") final Long adHocId) {
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.deleteAdHoc(adHocId) //
.build();
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteAdHoc(adHocId).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
@@ -55,18 +55,15 @@ public class AdHocReadPlatformServiceImpl implements AdHocReadPlatformService {
@Override
public Collection<AdHocData> retrieveAllActiveAdHocQuery() {
final String sql = "select " + this.adHocRowMapper.schema() + " where r." + sqlGenerator.escape("is_active")
+ " = true order by r.id";
final String sql = "select " + adHocRowMapper.schema() + " where r." + sqlGenerator.escape("is_active") + " = true order by r.id";
return this.jdbcTemplate.query(sql, this.adHocRowMapper); // NOSONAR
return jdbcTemplate.query(sql, adHocRowMapper); // NOSONAR
}
@Override
public AdHocData retrieveOne(final Long id) {
try {
final String sql = "select " + this.adHocRowMapper.schema() + " where r.id=?";
return this.jdbcTemplate.queryForObject(sql, this.adHocRowMapper, new Object[] { id }); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new AdHocNotFoundException(id, e);
@@ -19,6 +19,8 @@
package org.apache.fineract.adhocquery.service;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.adhocquery.domain.AdHoc;
import org.apache.fineract.adhocquery.domain.AdHocRepository;
import org.apache.fineract.adhocquery.exception.AdHocNotFoundException;
@@ -27,9 +29,6 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException;
@@ -37,22 +36,14 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
@RequiredArgsConstructor
public class AdHocWritePlatformServiceJpaRepositoryImpl implements AdHocWritePlatformService {
private static final Logger LOG = LoggerFactory.getLogger(AdHocWritePlatformServiceJpaRepositoryImpl.class);
private final PlatformSecurityContext context;
private final AdHocRepository adHocRepository;
private final AdHocDataValidator adHocCommandFromApiJsonDeserializer;
@Autowired
public AdHocWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final AdHocRepository adHocRepository,
final AdHocDataValidator adHocCommandFromApiJsonDeserializer) {
this.context = context;
this.adHocRepository = adHocRepository;
this.adHocCommandFromApiJsonDeserializer = adHocCommandFromApiJsonDeserializer;
}
@Transactional
@Override
public CommandProcessingResult createAdHocQuery(final JsonCommand command) {
@@ -69,9 +60,7 @@ public class AdHocWritePlatformServiceJpaRepositoryImpl implements AdHocWritePla
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
handleDataIntegrityIssues(command, throwable, dve);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.build();
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).build();
}
}
@@ -88,7 +77,7 @@ public class AdHocWritePlatformServiceJpaRepositoryImpl implements AdHocWritePla
"AdHocQuery with name `" + name + "` already exists", "name", name);
}
LOG.error("Error occured.", dve);
log.error("Error occured.", dve);
throw new PlatformDataIntegrityException("error.msg.adhocquery.unknown.data.integrity.issue",
"Unknown data integrity issue with resource.");
}
@@ -108,17 +97,11 @@ public class AdHocWritePlatformServiceJpaRepositoryImpl implements AdHocWritePla
this.adHocRepository.saveAndFlush(adHoc);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(adHocId) //
.with(changes) //
.build();
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(adHocId).with(changes).build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
handleDataIntegrityIssues(command, throwable, dve);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.build();
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).build();
}
}
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.infrastructure.businessdate.service;
import java.util.Map;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateData;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
@@ -26,6 +28,8 @@ public interface BusinessDateWritePlatformService {
CommandProcessingResult updateBusinessDate(JsonCommand command);
void adjustDate(BusinessDateData data, Map<String, Object> changes);
void increaseCOBDateByOneDay() throws JobExecutionException;
void increaseBusinessDateByOneDay() throws JobExecutionException;
@@ -42,9 +42,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.stereotype.Service;
@Slf4j
@@ -65,7 +63,8 @@ public class BusinessDateWritePlatformServiceImpl implements BusinessDateWritePl
}
private void adjustDate(BusinessDateData data, Map<String, Object> changes) {
@Override
public void adjustDate(BusinessDateData data, Map<String, Object> changes) {
boolean isCOBDateAdjustmentEnabled = configurationDomainService.isCOBDateAdjustmentEnabled();
boolean isBusinessDateEnabled = configurationDomainService.isBusinessDateEnabled();
@@ -80,21 +79,19 @@ public class BusinessDateWritePlatformServiceImpl implements BusinessDateWritePl
}
@Override
@CronTarget(jobName = JobName.INCREASE_COB_DATE_BY_1_DAY)
public void increaseCOBDateByOneDay() throws JobExecutionException {
increaseDateByTypeByOneDay(BusinessDateType.COB_DATE);
}
@Override
@CronTarget(jobName = JobName.INCREASE_BUSINESS_DATE_BY_1_DAY)
public void increaseBusinessDateByOneDay() throws JobExecutionException {
increaseDateByTypeByOneDay(BusinessDateType.BUSINESS_DATE);
}
private void increaseDateByTypeByOneDay(BusinessDateType businessDateType) throws JobExecutionException {
List<Throwable> exceptions = new ArrayList<>();
Map<String, Object> changes = new HashMap<>();
Optional<BusinessDate> businessDateEntity = repository.findByType(businessDateType);
List<Throwable> exceptions = new ArrayList<>();
LocalDate businessDate = businessDateEntity.map(BusinessDate::getDate).orElse(DateUtils.getLocalDateOfTenant());
businessDate = businessDate.plusDays(1);
@@ -114,7 +111,6 @@ public class BusinessDateWritePlatformServiceImpl implements BusinessDateWritePl
log.error("Increasing {} by 1 day failed due to: {}", businessDateType.getDescription(), e.getMessage());
exceptions.add(e);
}
if (!exceptions.isEmpty()) {
throw new JobExecutionException(exceptions);
}
@@ -32,6 +32,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.journalentry.api.DateParam;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
@@ -45,7 +46,6 @@ import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSer
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@@ -54,32 +54,22 @@ import org.springframework.stereotype.Component;
@Produces({ MediaType.APPLICATION_JSON })
@Component
@Scope("singleton")
@RequiredArgsConstructor
public class EmailApiResource {
private final String resourceNameForPermissions = "Email";
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "Email";
private final PlatformSecurityContext context;
private final EmailReadPlatformService readPlatformService;
private final DefaultToApiJsonSerializer<EmailData> toApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@Autowired
public EmailApiResource(final PlatformSecurityContext context, final EmailReadPlatformService readPlatformService,
final DefaultToApiJsonSerializer<EmailData> toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper,
final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService) {
this.context = context;
this.readPlatformService = readPlatformService;
this.toApiJsonSerializer = toApiJsonSerializer;
this.apiRequestParameterHelper = apiRequestParameterHelper;
this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
}
@GET
public String retrieveAllEmails(@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
final Collection<EmailData> emailMessages = this.readPlatformService.retrieveAll();
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, emailMessages);
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final Collection<EmailData> emailMessages = readPlatformService.retrieveAll();
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, emailMessages);
}
@GET
@@ -88,11 +78,11 @@ public class EmailApiResource {
@QueryParam("limit") final Integer limit, @QueryParam("orderBy") final String orderBy,
@QueryParam("sortOrder") final String sortOrder, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final SearchParameters searchParameters = SearchParameters.forEmailCampaign(sqlSearch, offset, limit, orderBy, sortOrder);
Collection<EmailData> emailMessages = this.readPlatformService.retrieveAllPending(searchParameters);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, emailMessages);
Collection<EmailData> emailMessages = readPlatformService.retrieveAllPending(searchParameters);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, emailMessages);
}
@GET
@@ -101,13 +91,13 @@ public class EmailApiResource {
@QueryParam("limit") final Integer limit, @QueryParam("orderBy") final String orderBy,
@QueryParam("sortOrder") final String sortOrder, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final SearchParameters searchParameters = SearchParameters.forEmailCampaign(sqlSearch, offset, limit, orderBy, sortOrder);
Collection<EmailData> emailMessages = this.readPlatformService.retrieveAllSent(searchParameters);
Collection<EmailData> emailMessages = readPlatformService.retrieveAllSent(searchParameters);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, emailMessages);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, emailMessages);
}
@GET
@@ -118,7 +108,7 @@ public class EmailApiResource {
@QueryParam("fromDate") final DateParam fromDateParam, @QueryParam("toDate") final DateParam toDateParam,
@QueryParam("locale") final String locale, @QueryParam("dateFormat") final String dateFormat, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
LocalDate fromDate = null;
if (fromDateParam != null) {
fromDate = fromDateParam.getDate("fromDate", dateFormat, locale);
@@ -127,9 +117,9 @@ public class EmailApiResource {
if (toDateParam != null) {
toDate = toDateParam.getDate("toDate", dateFormat, locale);
}
Page<EmailData> emailMessages = this.readPlatformService.retrieveEmailByStatus(limit, status, fromDate, toDate);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, emailMessages);
Page<EmailData> emailMessages = readPlatformService.retrieveEmailByStatus(limit, status, fromDate, toDate);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, emailMessages);
}
@GET
@@ -138,13 +128,13 @@ public class EmailApiResource {
@QueryParam("limit") final Integer limit, @QueryParam("orderBy") final String orderBy,
@QueryParam("sortOrder") final String sortOrder, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final SearchParameters searchParameters = SearchParameters.forEmailCampaign(sqlSearch, offset, limit, orderBy, sortOrder);
Collection<EmailData> emailMessages = this.readPlatformService.retrieveAllFailed(searchParameters);
Collection<EmailData> emailMessages = readPlatformService.retrieveAllFailed(searchParameters);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, emailMessages);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, emailMessages);
}
@POST
@@ -152,19 +142,19 @@ public class EmailApiResource {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createEmail().withJson(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
return toApiJsonSerializer.serialize(result);
}
@GET
@Path("{resourceId}")
public String retrieveOne(@PathParam("resourceId") final Long resourceId, @Context final UriInfo uriInfo) {
final EmailData emailMessage = this.readPlatformService.retrieveOne(resourceId);
final EmailData emailMessage = readPlatformService.retrieveOne(resourceId);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, emailMessage);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, emailMessage);
}
@PUT
@@ -173,9 +163,9 @@ public class EmailApiResource {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateEmail(resourceId).withJson(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
return toApiJsonSerializer.serialize(result);
}
@DELETE
@@ -184,8 +174,8 @@ public class EmailApiResource {
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteEmail(resourceId).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
return toApiJsonSerializer.serialize(result);
}
}
@@ -33,6 +33,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
@@ -49,7 +50,6 @@ import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSeria
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@@ -62,10 +62,10 @@ import org.springframework.stereotype.Component;
@Produces({ MediaType.APPLICATION_JSON })
@Component
@Scope("singleton")
@RequiredArgsConstructor
public class EmailCampaignApiResource {
// change name to email campaign
private final String resourceNameForPermissions = "EMAIL_CAMPAIGN";
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "EMAIL_CAMPAIGN";
private final PlatformSecurityContext context;
@@ -82,35 +82,15 @@ public class EmailCampaignApiResource {
private final DefaultToApiJsonSerializer<PreviewCampaignMessage> previewCampaignMessageDefaultToApiJsonSerializer;
@Autowired
public EmailCampaignApiResource(final PlatformSecurityContext context,
final DefaultToApiJsonSerializer<EmailBusinessRulesData> toApiJsonSerializer,
final ApiRequestParameterHelper apiRequestParameterHelper,
final EmailCampaignReadPlatformService emailCampaignReadPlatformService,
final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService,
final DefaultToApiJsonSerializer<EmailCampaignData> emailCampaignDataDefaultToApiJsonSerializer,
final FromJsonHelper fromJsonHelper, final EmailCampaignWritePlatformService emailCampaignWritePlatformService,
final DefaultToApiJsonSerializer<PreviewCampaignMessage> previewCampaignMessageDefaultToApiJsonSerializer) {
this.context = context;
this.toApiJsonSerializer = toApiJsonSerializer;
this.apiRequestParameterHelper = apiRequestParameterHelper;
this.emailCampaignReadPlatformService = emailCampaignReadPlatformService;
this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
this.emailCampaignDataDefaultToApiJsonSerializer = emailCampaignDataDefaultToApiJsonSerializer;
this.fromJsonHelper = fromJsonHelper;
this.emailCampaignWritePlatformService = emailCampaignWritePlatformService;
this.previewCampaignMessageDefaultToApiJsonSerializer = previewCampaignMessageDefaultToApiJsonSerializer;
}
@GET
@Path("{resourceId}")
@Produces({ MediaType.APPLICATION_JSON })
public String retrieveOneCampaign(@PathParam("resourceId") final Long resourceId, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
EmailCampaignData emailCampaignData = this.emailCampaignReadPlatformService.retrieveOne(resourceId);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.emailCampaignDataDefaultToApiJsonSerializer.serialize(settings, emailCampaignData);
EmailCampaignData emailCampaignData = emailCampaignReadPlatformService.retrieveOne(resourceId);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return emailCampaignDataDefaultToApiJsonSerializer.serialize(settings, emailCampaignData);
}
@@ -118,12 +98,12 @@ public class EmailCampaignApiResource {
@Produces({ MediaType.APPLICATION_JSON })
public String retrieveAllCampaign(@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final Collection<EmailCampaignData> emailCampaignDataCollection = this.emailCampaignReadPlatformService.retrieveAllCampaign();
final Collection<EmailCampaignData> emailCampaignDataCollection = emailCampaignReadPlatformService.retrieveAllCampaign();
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.emailCampaignDataDefaultToApiJsonSerializer.serialize(settings, emailCampaignDataCollection);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return emailCampaignDataDefaultToApiJsonSerializer.serialize(settings, emailCampaignDataCollection);
}
@POST
@@ -133,9 +113,9 @@ public class EmailCampaignApiResource {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createEmailCampaign().withJson(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
return toApiJsonSerializer.serialize(result);
}
@PUT
@@ -148,9 +128,9 @@ public class EmailCampaignApiResource {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateEmailCampaign(campaignId).withJson(apiRequestBodyAsJson)
.build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
return toApiJsonSerializer.serialize(result);
}
@POST
@@ -162,18 +142,18 @@ public class EmailCampaignApiResource {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandProcessingResult result = null;
CommandWrapper commandRequest = null;
CommandWrapper commandRequest;
if (is(commandParam, "activate")) {
commandRequest = builder.activateEmailCampaign(campaignId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "close")) {
commandRequest = builder.closeEmailCampaign(campaignId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "reactivate")) {
commandRequest = builder.reactivateEmailCampaign(campaignId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
return this.toApiJsonSerializer.serialize(result);
return toApiJsonSerializer.serialize(result);
}
@POST
@@ -181,36 +161,36 @@ public class EmailCampaignApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String preview(final String apiRequestBodyAsJson, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
PreviewCampaignMessage campaignMessage = null;
final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson);
final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper);
campaignMessage = this.emailCampaignWritePlatformService.previewMessage(query);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.previewCampaignMessageDefaultToApiJsonSerializer.serialize(settings, campaignMessage, new HashSet<String>());
PreviewCampaignMessage campaignMessage;
final JsonElement parsedQuery = fromJsonHelper.parse(apiRequestBodyAsJson);
final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, fromJsonHelper);
campaignMessage = emailCampaignWritePlatformService.previewMessage(query);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return previewCampaignMessageDefaultToApiJsonSerializer.serialize(settings, campaignMessage, new HashSet<>());
}
@GET()
@Path("template")
public String template(@Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final Collection<EmailBusinessRulesData> emailBusinessRulesDataCollection = this.emailCampaignReadPlatformService.retrieveAll();
final Collection<EmailBusinessRulesData> emailBusinessRulesDataCollection = emailCampaignReadPlatformService.retrieveAll();
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, emailBusinessRulesDataCollection);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, emailBusinessRulesDataCollection);
}
@GET
@Path("template/{resourceId}")
public String retrieveOneTemplate(@PathParam("resourceId") final Long resourceId, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final EmailBusinessRulesData emailBusinessRulesData = this.emailCampaignReadPlatformService.retrieveOneTemplate(resourceId);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, emailBusinessRulesData);
final EmailBusinessRulesData emailBusinessRulesData = emailCampaignReadPlatformService.retrieveOneTemplate(resourceId);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, emailBusinessRulesData);
}
@@ -220,9 +200,9 @@ public class EmailCampaignApiResource {
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteEmailCampaign(resourceId).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
return toApiJsonSerializer.serialize(result);
}
private boolean is(final String commandParam, final String commandValue) {
@@ -24,9 +24,6 @@ import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import com.google.gson.Gson;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
@@ -36,8 +33,6 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -46,17 +41,14 @@ import java.util.Map;
import java.util.Set;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.campaigns.email.data.EmailCampaignData;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.campaigns.email.data.EmailCampaignValidator;
import org.apache.fineract.infrastructure.campaigns.email.data.EmailMessageWithAttachmentData;
import org.apache.fineract.infrastructure.campaigns.email.data.PreviewCampaignMessage;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaign;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaignRepository;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessage;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageRepository;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageStatusType;
import org.apache.fineract.infrastructure.campaigns.email.domain.ScheduledEmailAttachmentFileFormat;
import org.apache.fineract.infrastructure.campaigns.email.exception.EmailCampaignMustBeClosedToBeDeletedException;
import org.apache.fineract.infrastructure.campaigns.email.exception.EmailCampaignMustBeClosedToEditException;
import org.apache.fineract.infrastructure.campaigns.email.exception.EmailCampaignNotFound;
@@ -76,23 +68,12 @@ import org.apache.fineract.infrastructure.dataqueries.domain.ReportRepository;
import org.apache.fineract.infrastructure.dataqueries.exception.ReportNotFoundException;
import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService;
import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.reportmailingjob.helper.IPv4Helper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException;
@@ -100,47 +81,19 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
@RequiredArgsConstructor
public class EmailCampaignWritePlatformCommandHandlerImpl implements EmailCampaignWritePlatformService {
private static final Logger LOG = LoggerFactory.getLogger(EmailCampaignWritePlatformCommandHandlerImpl.class);
private final PlatformSecurityContext context;
private final EmailCampaignRepository emailCampaignRepository;
private final EmailCampaignValidator emailCampaignValidator;
private final EmailCampaignReadPlatformService emailCampaignReadPlatformService;
private final ReportRepository reportRepository;
private final EmailMessageRepository emailMessageRepository;
private final ClientRepositoryWrapper clientRepositoryWrapper;
private final ReadReportingService readReportingService;
private final GenericDataService genericDataService;
private final FromJsonHelper fromJsonHelper;
private final LoanRepository loanRepository;
private final SavingsAccountRepository savingsAccountRepository;
private final EmailMessageJobEmailService emailMessageJobEmailService;
@Autowired
public EmailCampaignWritePlatformCommandHandlerImpl(final PlatformSecurityContext context,
final EmailCampaignRepository emailCampaignRepository, final EmailCampaignValidator emailCampaignValidator,
final EmailCampaignReadPlatformService emailCampaignReadPlatformService, final ReportRepository reportRepository,
final EmailMessageRepository emailMessageRepository, final ClientRepositoryWrapper clientRepositoryWrapper,
final ReadReportingService readReportingService, final GenericDataService genericDataService,
final FromJsonHelper fromJsonHelper, final LoanRepository loanRepository,
final SavingsAccountRepository savingsAccountRepository, final EmailMessageJobEmailService emailMessageJobEmailService) {
this.context = context;
this.emailCampaignRepository = emailCampaignRepository;
this.emailCampaignValidator = emailCampaignValidator;
this.emailCampaignReadPlatformService = emailCampaignReadPlatformService;
this.reportRepository = reportRepository;
this.emailMessageRepository = emailMessageRepository;
this.clientRepositoryWrapper = clientRepositoryWrapper;
this.readReportingService = readReportingService;
this.genericDataService = genericDataService;
this.fromJsonHelper = fromJsonHelper;
this.loanRepository = loanRepository;
this.savingsAccountRepository = savingsAccountRepository;
this.emailMessageJobEmailService = emailMessageJobEmailService;
}
@Transactional
@Override
@@ -178,10 +131,7 @@ public class EmailCampaignWritePlatformCommandHandlerImpl implements EmailCampai
this.emailCampaignRepository.saveAndFlush(emailCampaign);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(emailCampaign.getId()) //
.build();
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(emailCampaign.getId()).build();
}
@Transactional
@@ -208,11 +158,7 @@ public class EmailCampaignWritePlatformCommandHandlerImpl implements EmailCampai
if (!changes.isEmpty()) {
this.emailCampaignRepository.saveAndFlush(emailCampaign);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(resourceId) //
.with(changes) //
.build();
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(resourceId).with(changes).build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
handleDataIntegrityIssues(command, throwable, dve);
@@ -319,56 +265,6 @@ public class EmailCampaignWritePlatformCommandHandlerImpl implements EmailCampai
return isValid;
}
@Override
@CronTarget(jobName = JobName.UPDATE_EMAIL_OUTBOUND_WITH_CAMPAIGN_MESSAGE)
public void storeTemplateMessageIntoEmailOutBoundTable() throws JobExecutionException {
final Collection<EmailCampaignData> emailCampaignDataCollection = this.emailCampaignReadPlatformService
.retrieveAllScheduleActiveCampaign();
if (emailCampaignDataCollection != null) {
for (EmailCampaignData emailCampaignData : emailCampaignDataCollection) {
LocalDateTime tenantDateNow = tenantDateTime();
LocalDateTime nextTriggerDate = emailCampaignData.getNextTriggerDate().toLocalDateTime();
LOG.info("tenant time {} trigger time {}", tenantDateNow, nextTriggerDate);
if (nextTriggerDate.isBefore(tenantDateNow)) {
insertDirectCampaignIntoEmailOutboundTable(emailCampaignData.getParamValue(), emailCampaignData.getEmailSubject(),
emailCampaignData.getMessage(), emailCampaignData.getCampaignName(), emailCampaignData.getId());
this.updateTriggerDates(emailCampaignData.getId());
}
}
}
}
private void updateTriggerDates(Long campaignId) {
final EmailCampaign emailCampaign = this.emailCampaignRepository.findById(campaignId)
.orElseThrow(() -> new EmailCampaignNotFound(campaignId));
LocalDateTime nextTriggerDate = emailCampaign.getNextTriggerDate();
emailCampaign.setLastTriggerDate(nextTriggerDate);
// calculate new trigger date and insert into next trigger date
/**
* next run time has to be in the future if not calculate a new future date
*/
LocalDateTime newTriggerDateWithTime = CalendarUtils.getNextRecurringDate(emailCampaign.getRecurrence(),
emailCampaign.getNextTriggerDate(), nextTriggerDate);
if (newTriggerDateWithTime.isBefore(DateUtils.getLocalDateTimeOfTenant())) { // means
// next
// run
// time is
// in the
// past
// calculate
// a new
// future
// date
newTriggerDateWithTime = CalendarUtils.getNextRecurringDate(emailCampaign.getRecurrence(), emailCampaign.getNextTriggerDate(),
DateUtils.getLocalDateTimeOfTenant());
}
emailCampaign.setNextTriggerDate(newTriggerDateWithTime);
this.emailCampaignRepository.saveAndFlush(emailCampaign);
}
@Transactional
@Override
public CommandProcessingResult activateEmailCampaign(Long campaignId, JsonCommand command) {
@@ -456,7 +352,8 @@ public class EmailCampaignWritePlatformCommandHandlerImpl implements EmailCampai
}
@SuppressWarnings({ "unused", "rawtypes" })
private List<HashMap<String, Object>> getRunReportByServiceImpl(final String reportName, final Map<String, String> queryParams)
@Override
public List<HashMap<String, Object>> getRunReportByServiceImpl(final String reportName, final Map<String, String> queryParams)
throws IOException {
final String reportType = "report";
@@ -585,248 +482,4 @@ public class EmailCampaignWritePlatformCommandHandlerImpl implements EmailCampai
}
return today;
}
@Override
@CronTarget(jobName = JobName.EXECUTE_EMAIL)
public void sendEmailMessage() throws JobExecutionException {
if (IPv4Helper.applicationIsNotRunningOnLocalMachine()) { // remove when
// testing
// locally
final List<EmailMessage> emailMessages = this.emailMessageRepository
.findByStatusType(EmailMessageStatusType.PENDING.getValue()); // retrieve
// all
// pending
// message
for (final EmailMessage emailMessage : emailMessages) {
if (isValidEmail(emailMessage.getEmailAddress())) {
final EmailCampaign emailCampaign = this.emailCampaignRepository.findById(emailMessage.getEmailCampaign().getId())
.orElse(null); //
ScheduledEmailAttachmentFileFormat emailAttachmentFileFormat = null;
if (emailCampaign.getEmailAttachmentFileFormat() != null) {
emailAttachmentFileFormat = ScheduledEmailAttachmentFileFormat
.instance(emailCampaign.getEmailAttachmentFileFormat());
}
final List<File> attachmentList = new ArrayList<>();
final StringBuilder errorLog = new StringBuilder();
// check if email attachment format exist
if (emailAttachmentFileFormat != null && Arrays.asList(ScheduledEmailAttachmentFileFormat.validValues())
.contains(emailAttachmentFileFormat.getId())) {
final Report stretchyReport = emailCampaign.getStretchyReport();
final String reportName = (stretchyReport != null) ? stretchyReport.getReportName() : null;
final HashMap<String, String> reportStretchyParams = this
.validateStretchyReportParamMap(emailCampaign.getStretchyReportParamMap());
// there is a probability that a client has one or more
// loans or savings therefore we need to send two or
// more attachments
if (reportStretchyParams.containsKey("selectLoan") || reportStretchyParams.containsKey("loanId")) {
// get all ids of the client loans
if (emailMessage.getClient() != null) {
final List<Loan> loans = this.loanRepository.findLoanByClientId(emailMessage.getClient().getId());
HashMap<String, String> reportParams = this
.replaceStretchyParamsWithActualClientParams(reportStretchyParams, emailMessage.getClient());
for (final Loan loan : loans) {
if (loan.isOpen()) { // only send attachment
// for active loan
if (reportStretchyParams.containsKey("selectLoan")) {
reportParams.put("SelectLoan", loan.getId().toString());
} else if (reportStretchyParams.containsKey("loanId")) {
reportParams.put("loanId", loan.getId().toString());
}
File file = this.generateAttachments(emailCampaign, emailAttachmentFileFormat, reportParams,
reportName, errorLog);
if (file != null) {
attachmentList.add(file);
} else {
errorLog.append(reportParams.toString());
}
}
}
}
} else if (reportStretchyParams.containsKey("savingId")) {
if (emailMessage.getClient() != null) {
final List<SavingsAccount> savingsAccounts = this.savingsAccountRepository
.findSavingAccountByClientId(emailMessage.getClient().getId());
HashMap<String, String> reportParams = this
.replaceStretchyParamsWithActualClientParams(reportStretchyParams, emailMessage.getClient());
for (final SavingsAccount savingsAccount : savingsAccounts) {
if (savingsAccount.isActive()) {
reportParams.put("savingId", savingsAccount.getId().toString());
File file = this.generateAttachments(emailCampaign, emailAttachmentFileFormat, reportParams,
reportName, errorLog);
if (file != null) {
attachmentList.add(file);
} else {
errorLog.append(reportParams.toString());
}
}
}
}
} else {
if (emailMessage.getClient() != null) {
HashMap<String, String> reportParams = this
.replaceStretchyParamsWithActualClientParams(reportStretchyParams, emailMessage.getClient());
File file = this.generateAttachments(emailCampaign, emailAttachmentFileFormat, reportParams, reportName,
errorLog);
if (file != null) {
attachmentList.add(file);
} else {
errorLog.append(reportParams.toString());
}
}
}
}
final EmailMessageWithAttachmentData emailMessageWithAttachmentData = EmailMessageWithAttachmentData.createNew(
emailMessage.getEmailAddress(), emailMessage.getMessage(), emailMessage.getEmailSubject(), attachmentList);
try {
this.emailMessageJobEmailService.sendEmailWithAttachment(emailMessageWithAttachmentData);
emailMessage.setStatusType(EmailMessageStatusType.SENT.getValue());
this.emailMessageRepository.save(emailMessage);
} catch (Exception e) {
emailMessage.updateErrorMessage(e.getMessage());
emailMessage.setStatusType(EmailMessageStatusType.FAILED.getValue());
this.emailMessageRepository.save(emailMessage);
}
}
}
}
}
/**
* This generates the the report and converts it to a file by passing the parameters below
*
* @param emailCampaign
* @param emailAttachmentFileFormat
* @param reportParams
* @param reportName
* @param errorLog
* @return
*/
private File generateAttachments(final EmailCampaign emailCampaign, final ScheduledEmailAttachmentFileFormat emailAttachmentFileFormat,
final Map<String, String> reportParams, final String reportName, final StringBuilder errorLog) {
if (reportName == null) {
return null;
}
try {
final ByteArrayOutputStream byteArrayOutputStream = this.readReportingService.generatePentahoReportAsOutputStream(reportName,
emailAttachmentFileFormat.getValue(), reportParams, null, emailCampaign.getApprovedBy(), errorLog);
final String fileLocation = FileSystemContentRepository.FINERACT_BASE_DIR + File.separator + "";
final String fileNameWithoutExtension = fileLocation + File.separator + reportName;
// check if file directory exists, if not create directory
if (!new File(fileLocation).isDirectory()) {
new File(fileLocation).mkdirs();
}
if (byteArrayOutputStream.size() == 0) {
errorLog.append("Pentaho report processing failed, empty output stream created");
} else if (errorLog.length() == 0 && (byteArrayOutputStream.size() > 0)) {
final String fileName = fileNameWithoutExtension + "." + emailAttachmentFileFormat.getValue();
final File file = new File(fileName);
final FileOutputStream outputStream = new FileOutputStream(file);
byteArrayOutputStream.writeTo(outputStream);
return file;
}
} catch (IOException | PlatformDataIntegrityException e) {
errorLog.append("The ReportMailingJobWritePlatformServiceImpl.executeReportMailingJobs threw an IOException " + "exception: "
+ e.getMessage() + " ---------- ");
}
return null;
}
/**
* This matches the the actual values to the key in the report stretchy parameters map
*
* @param stretchyParams
* @param client
* @return
*/
private HashMap<String, String> replaceStretchyParamsWithActualClientParams(final HashMap<String, String> stretchyParams,
final Client client) {
HashMap<String, String> actualParams = new HashMap<>();
for (Map.Entry<String, String> entry : stretchyParams.entrySet()) {
if (entry.getKey().equals("selectOffice")) {
// most at times the reports are run by picking the office of
// the staff Id
if (client.getStaff() != null) {
actualParams.put(entry.getKey(), client.getStaff().officeId().toString());
} else {
actualParams.put(entry.getKey(), client.getOffice().getId().toString());
}
} else if (entry.getKey().equals("selectClient")) {
actualParams.put(entry.getKey(), client.getId().toString());
} else if (entry.getKey().equals("selectLoanofficer")) {
actualParams.put(entry.getKey(), client.getStaff().getId().toString());
} else if (entry.getKey().equals("environementUrl")) {
actualParams.put(entry.getKey(), entry.getKey());
}
}
return actualParams;
}
private HashMap<String, String> validateStretchyReportParamMap(final String stretchyParams) {
HashMap<String, String> stretchyReportParamHashMap = new HashMap<>();
if (!StringUtils.isEmpty(stretchyParams)) {
try {
stretchyReportParamHashMap = new ObjectMapper().readValue(stretchyParams, new TypeReference<HashMap<String, String>>() {});
}
catch (Exception e) {
stretchyReportParamHashMap = null;
}
}
return stretchyReportParamHashMap;
}
}
@@ -18,13 +18,15 @@
*/
package org.apache.fineract.infrastructure.campaigns.email.service;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.infrastructure.campaigns.email.data.PreviewCampaignMessage;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaign;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
public interface EmailCampaignWritePlatformService {
@@ -41,12 +43,11 @@ public interface EmailCampaignWritePlatformService {
CommandProcessingResult reactivateEmailCampaign(Long campaignId, JsonCommand command);
void storeTemplateMessageIntoEmailOutBoundTable() throws JobExecutionException;
@SuppressWarnings({ "unused", "rawtypes" })
List<HashMap<String, Object>> getRunReportByServiceImpl(String reportName, Map<String, String> queryParams) throws IOException;
PreviewCampaignMessage previewMessage(JsonQuery query);
void sendEmailMessage() throws JobExecutionException;
void insertDirectCampaignIntoEmailOutboundTable(Loan loan, EmailCampaign emailCampaign, HashMap<String, String> campaignParams);
}
@@ -0,0 +1,41 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.campaigns.jobs;
import java.time.LocalDateTime;
import java.time.ZoneId;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
public final class TenantDateTimeUtil {
private TenantDateTimeUtil() {}
public static LocalDateTime tenantDateTime() {
LocalDateTime today = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
final FineractPlatformTenant tenant = ThreadLocalContextUtil.getTenant();
if (tenant != null) {
final ZoneId zone = ZoneId.of(tenant.getTimezoneId());
today = LocalDateTime.now(zone);
}
return today;
}
}
@@ -0,0 +1,75 @@
/**
* 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.campaigns.jobs.executeemail;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaignRepository;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageRepository;
import org.apache.fineract.infrastructure.campaigns.email.service.EmailMessageJobEmailService;
import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.reportmailingjob.validation.ReportMailingJobValidator;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ExecuteEmailConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private EmailMessageRepository emailMessageRepository;
@Autowired
private EmailCampaignRepository emailCampaignRepository;
@Autowired
private LoanRepository loanRepository;
@Autowired
private SavingsAccountRepository savingsAccountRepository;
@Autowired
private EmailMessageJobEmailService emailMessageJobEmailService;
@Autowired
private ReadReportingService readReportingService;
@Autowired
private ReportMailingJobValidator reportMailingJobValidator;
@Bean
protected Step executeEmailStep() {
return steps.get(JobName.EXECUTE_EMAIL.name()).tasklet(executeEmailTasklet()).build();
}
@Bean
public Job executeEmailJob() {
return jobs.get(JobName.EXECUTE_EMAIL.name()).start(executeEmailStep()).incrementer(new RunIdIncrementer()).build();
}
@Bean
public ExecuteEmailTasklet executeEmailTasklet() {
return new ExecuteEmailTasklet(emailMessageRepository, emailCampaignRepository, loanRepository, savingsAccountRepository,
emailMessageJobEmailService, readReportingService, reportMailingJobValidator);
}
}
@@ -0,0 +1,229 @@
/**
* 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.campaigns.jobs.executeemail;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.campaigns.email.data.EmailMessageWithAttachmentData;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaign;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaignRepository;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessage;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageRepository;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageStatusType;
import org.apache.fineract.infrastructure.campaigns.email.domain.ScheduledEmailAttachmentFileFormat;
import org.apache.fineract.infrastructure.campaigns.email.service.EmailMessageJobEmailService;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.dataqueries.domain.Report;
import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository;
import org.apache.fineract.infrastructure.reportmailingjob.helper.IPv4Helper;
import org.apache.fineract.infrastructure.reportmailingjob.validation.ReportMailingJobValidator;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class ExecuteEmailTasklet implements Tasklet {
private final EmailMessageRepository emailMessageRepository;
private final EmailCampaignRepository emailCampaignRepository;
private final LoanRepository loanRepository;
private final SavingsAccountRepository savingsAccountRepository;
private final EmailMessageJobEmailService emailMessageJobEmailService;
private final ReadReportingService readReportingService;
private final ReportMailingJobValidator reportMailingJobValidator;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
if (IPv4Helper.applicationIsNotRunningOnLocalMachine()) {
final List<EmailMessage> emailMessages = emailMessageRepository.findByStatusType(EmailMessageStatusType.PENDING.getValue());
for (final EmailMessage emailMessage : emailMessages) {
if (isValidEmail(emailMessage.getEmailAddress())) {
final EmailCampaign emailCampaign = emailCampaignRepository.findById(emailMessage.getEmailCampaign().getId())
.orElse(null); //
ScheduledEmailAttachmentFileFormat emailAttachmentFileFormat = null;
if (emailCampaign.getEmailAttachmentFileFormat() != null) {
emailAttachmentFileFormat = ScheduledEmailAttachmentFileFormat
.instance(emailCampaign.getEmailAttachmentFileFormat());
}
final List<File> attachmentList = new ArrayList<>();
final StringBuilder errorLog = new StringBuilder();
if (emailAttachmentFileFormat != null && Arrays.asList(ScheduledEmailAttachmentFileFormat.validValues())
.contains(emailAttachmentFileFormat.getId())) {
final Report stretchyReport = emailCampaign.getStretchyReport();
final String reportName = (stretchyReport != null) ? stretchyReport.getReportName() : null;
final HashMap<String, String> reportStretchyParams = reportMailingJobValidator
.validateStretchyReportParamMap(emailCampaign.getStretchyReportParamMap());
if (reportStretchyParams.containsKey("selectLoan") || reportStretchyParams.containsKey("loanId")) {
if (emailMessage.getClient() != null) {
final List<Loan> loans = loanRepository.findLoanByClientId(emailMessage.getClient().getId());
HashMap<String, String> reportParams = replaceStretchyParamsWithActualClientParams(reportStretchyParams,
emailMessage.getClient());
for (final Loan loan : loans) {
if (loan.isOpen()) {
if (reportStretchyParams.containsKey("selectLoan")) {
reportParams.put("SelectLoan", loan.getId().toString());
} else if (reportStretchyParams.containsKey("loanId")) {
reportParams.put("loanId", loan.getId().toString());
}
File file = generateAttachments(emailCampaign, emailAttachmentFileFormat, reportParams, reportName,
errorLog);
if (file != null) {
attachmentList.add(file);
} else {
errorLog.append(reportParams);
}
}
}
}
} else if (reportStretchyParams.containsKey("savingId")) {
if (emailMessage.getClient() != null) {
final List<SavingsAccount> savingsAccounts = savingsAccountRepository
.findSavingAccountByClientId(emailMessage.getClient().getId());
HashMap<String, String> reportParams = replaceStretchyParamsWithActualClientParams(reportStretchyParams,
emailMessage.getClient());
for (final SavingsAccount savingsAccount : savingsAccounts) {
if (savingsAccount.isActive()) {
reportParams.put("savingId", savingsAccount.getId().toString());
File file = generateAttachments(emailCampaign, emailAttachmentFileFormat, reportParams, reportName,
errorLog);
if (file != null) {
attachmentList.add(file);
} else {
errorLog.append(reportParams);
}
}
}
}
} else {
if (emailMessage.getClient() != null) {
HashMap<String, String> reportParams = replaceStretchyParamsWithActualClientParams(reportStretchyParams,
emailMessage.getClient());
File file = generateAttachments(emailCampaign, emailAttachmentFileFormat, reportParams, reportName,
errorLog);
if (file != null) {
attachmentList.add(file);
} else {
errorLog.append(reportParams);
}
}
}
}
final EmailMessageWithAttachmentData emailMessageWithAttachmentData = EmailMessageWithAttachmentData.createNew(
emailMessage.getEmailAddress(), emailMessage.getMessage(), emailMessage.getEmailSubject(), attachmentList);
try {
emailMessageJobEmailService.sendEmailWithAttachment(emailMessageWithAttachmentData);
emailMessage.setStatusType(EmailMessageStatusType.SENT.getValue());
emailMessageRepository.save(emailMessage);
} catch (Exception e) {
emailMessage.updateErrorMessage(e.getMessage());
emailMessage.setStatusType(EmailMessageStatusType.FAILED.getValue());
emailMessageRepository.save(emailMessage);
}
}
}
}
return RepeatStatus.FINISHED;
}
public static boolean isValidEmail(String email) {
boolean isValid = true;
try {
InternetAddress emailO = new InternetAddress(email);
emailO.validate();
} catch (AddressException ex) {
isValid = false;
}
return isValid;
}
private HashMap<String, String> replaceStretchyParamsWithActualClientParams(final HashMap<String, String> stretchyParams,
final Client client) {
HashMap<String, String> actualParams = new HashMap<>();
for (Map.Entry<String, String> entry : stretchyParams.entrySet()) {
switch (entry.getKey()) {
case "selectOffice":
if (client.getStaff() != null) {
actualParams.put(entry.getKey(), client.getStaff().officeId().toString());
} else {
actualParams.put(entry.getKey(), client.getOffice().getId().toString());
}
break;
case "selectClient":
actualParams.put(entry.getKey(), client.getId().toString());
break;
case "selectLoanofficer":
actualParams.put(entry.getKey(), client.getStaff().getId().toString());
break;
case "environementUrl":
actualParams.put(entry.getKey(), entry.getKey());
break;
}
}
return actualParams;
}
private File generateAttachments(final EmailCampaign emailCampaign, final ScheduledEmailAttachmentFileFormat emailAttachmentFileFormat,
final Map<String, String> reportParams, final String reportName, final StringBuilder errorLog) {
if (reportName == null) {
return null;
}
try {
final ByteArrayOutputStream byteArrayOutputStream = readReportingService.generatePentahoReportAsOutputStream(reportName,
emailAttachmentFileFormat.getValue(), reportParams, null, emailCampaign.getApprovedBy(), errorLog);
final String fileLocation = FileSystemContentRepository.FINERACT_BASE_DIR + File.separator + "";
final String fileNameWithoutExtension = fileLocation + File.separator + reportName;
if (!new File(fileLocation).isDirectory()) {
new File(fileLocation).mkdirs();
}
if (byteArrayOutputStream.size() == 0) {
errorLog.append("Pentaho report processing failed, empty output stream created");
} else if (errorLog.length() == 0 && (byteArrayOutputStream.size() > 0)) {
final String fileName = fileNameWithoutExtension + "." + emailAttachmentFileFormat.getValue();
final File file = new File(fileName);
final FileOutputStream outputStream = new FileOutputStream(file);
byteArrayOutputStream.writeTo(outputStream);
return file;
}
} catch (IOException | PlatformDataIntegrityException e) {
errorLog.append("The ReportMailingJobWritePlatformServiceImpl.executeReportMailingJobs threw an IOException " + "exception: ")
.append(e.getMessage()).append(" ---------- ");
}
return null;
}
}
@@ -0,0 +1,74 @@
/**
* 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.campaigns.jobs.executereportmailingjobs;
import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.report.provider.ReportingProcessServiceProvider;
import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRepository;
import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRunHistoryRepository;
import org.apache.fineract.infrastructure.reportmailingjob.service.ReportMailingJobEmailService;
import org.apache.fineract.infrastructure.reportmailingjob.validation.ReportMailingJobValidator;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ExecuteReportMailingJobsConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private ReportMailingJobRepository reportMailingJobRepository;
@Autowired
private ReportMailingJobValidator reportMailingJobValidator;
@Autowired
private ReadReportingService readReportingService;
@Autowired
private ReportingProcessServiceProvider reportingProcessServiceProvider;
@Autowired
private ReportMailingJobEmailService reportMailingJobEmailService;
@Autowired
private ReportMailingJobRunHistoryRepository reportMailingJobRunHistoryRepository;
@Bean
protected Step executeReportMailingJobsStep() {
return steps.get(JobName.EXECUTE_REPORT_MAILING_JOBS.name()).tasklet(executeReportMailingJobsTasklet()).build();
}
@Bean
public Job executeReportMailingJobsJob() {
return jobs.get(JobName.EXECUTE_REPORT_MAILING_JOBS.name()).start(executeReportMailingJobsStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public ExecuteReportMailingJobsTasklet executeReportMailingJobsTasklet() {
return new ExecuteReportMailingJobsTasklet(reportMailingJobRepository, reportMailingJobValidator, readReportingService,
reportingProcessServiceProvider, reportMailingJobEmailService, reportMailingJobRunHistoryRepository);
}
}
@@ -0,0 +1,245 @@
/**
* 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.campaigns.jobs.executereportmailingjobs;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.dataqueries.domain.Report;
import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository;
import org.apache.fineract.infrastructure.report.provider.ReportingProcessServiceProvider;
import org.apache.fineract.infrastructure.report.service.ReportingProcessService;
import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobEmailAttachmentFileFormat;
import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobEmailData;
import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobPreviousRunStatus;
import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobStretchyReportParamDateOption;
import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJob;
import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRepository;
import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRunHistory;
import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRunHistoryRepository;
import org.apache.fineract.infrastructure.reportmailingjob.service.ReportMailingJobEmailService;
import org.apache.fineract.infrastructure.reportmailingjob.util.ReportMailingJobDateUtil;
import org.apache.fineract.infrastructure.reportmailingjob.validation.ReportMailingJobValidator;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.glassfish.jersey.internal.util.collection.MultivaluedStringMap;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class ExecuteReportMailingJobsTasklet implements Tasklet {
private final ReportMailingJobRepository reportMailingJobRepository;
private final ReportMailingJobValidator reportMailingJobValidator;
private final ReadReportingService readReportingService;
private final ReportingProcessServiceProvider reportingProcessServiceProvider;
private final ReportMailingJobEmailService reportMailingJobEmailService;
private final ReportMailingJobRunHistoryRepository reportMailingJobRunHistoryRepository;
private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final Collection<ReportMailingJob> reportMailingJobCollection = reportMailingJobRepository.findByIsActiveTrueAndIsDeletedFalse();
for (ReportMailingJob reportMailingJob : reportMailingJobCollection) {
final LocalDateTime localDateTimeOftenant = DateUtils.getLocalDateTimeOfTenant();
final LocalDateTime nextRunDateTime = reportMailingJob.getNextRunDateTime();
if (nextRunDateTime != null && nextRunDateTime.isBefore(localDateTimeOftenant)) {
final ReportMailingJobEmailAttachmentFileFormat emailAttachmentFileFormat = ReportMailingJobEmailAttachmentFileFormat
.newInstance(reportMailingJob.getEmailAttachmentFileFormat());
if (emailAttachmentFileFormat != null && emailAttachmentFileFormat.isValid()) {
final Report stretchyReport = reportMailingJob.getStretchyReport();
final String reportName = (stretchyReport != null) ? stretchyReport.getReportName() : null;
final StringBuilder errorLog = new StringBuilder();
final Map<String, String> validateStretchyReportParamMap = reportMailingJobValidator
.validateStretchyReportParamMap(reportMailingJob.getStretchyReportParamMap());
MultivaluedMap<String, String> reportParams = new MultivaluedStringMap();
if (validateStretchyReportParamMap != null) {
for (Map.Entry<String, String> validateStretchyReportParamMapEntry : validateStretchyReportParamMap.entrySet()) {
String key = validateStretchyReportParamMapEntry.getKey();
String value = validateStretchyReportParamMapEntry.getValue();
if (StringUtils.containsIgnoreCase(key, "date")) {
ReportMailingJobStretchyReportParamDateOption reportMailingJobStretchyReportParamDateOption = ReportMailingJobStretchyReportParamDateOption
.newInstance(value);
if (reportMailingJobStretchyReportParamDateOption.isValid()) {
value = ReportMailingJobDateUtil.getDateAsString(reportMailingJobStretchyReportParamDateOption);
}
}
reportParams.add(key, value);
}
}
generateReportOutputStream(reportMailingJob, emailAttachmentFileFormat, reportParams, reportName, errorLog);
updateReportMailingJobAfterJobExecution(reportMailingJob, errorLog, localDateTimeOftenant);
}
}
}
return RepeatStatus.FINISHED;
}
private void generateReportOutputStream(final ReportMailingJob reportMailingJob,
final ReportMailingJobEmailAttachmentFileFormat emailAttachmentFileFormat, final MultivaluedMap<String, String> reportParams,
final String reportName, final StringBuilder errorLog) {
try {
final boolean isSelfServiceUserReport = false;
final String reportType = readReportingService.getReportType(reportName, isSelfServiceUserReport, false);
final ReportingProcessService reportingProcessService = reportingProcessServiceProvider.findReportingProcessService(reportType);
if (reportingProcessService != null) {
final Response processReport = reportingProcessService.processRequest(reportName, reportParams);
final Object responseObject = (processReport != null) ? processReport.getEntity() : null;
if (responseObject != null && responseObject.getClass().equals(ByteArrayOutputStream.class)) {
final ByteArrayOutputStream byteArrayOutputStream = (ByteArrayOutputStream) responseObject;
final String fileLocation = FileSystemContentRepository.FINERACT_BASE_DIR + File.separator + "";
final String fileNameWithoutExtension = fileLocation + File.separator + reportName;
if (!new File(fileLocation).isDirectory()) {
new File(fileLocation).mkdirs();
}
if (byteArrayOutputStream.size() == 0) {
errorLog.append("Report processing failed, empty output stream created");
} else if ((errorLog != null && errorLog.length() == 0) && (byteArrayOutputStream.size() > 0)) {
final String fileName = fileNameWithoutExtension + "." + emailAttachmentFileFormat.getValue();
sendReportFileToEmailRecipients(reportMailingJob, fileName, byteArrayOutputStream, errorLog);
}
} else {
errorLog.append("Response object entity is not equal to ByteArrayOutputStream ---------- ");
}
} else {
errorLog.append(ReportingProcessServiceProvider.SERVICE_MISSING).append(reportType);
}
} catch (Exception e) {
errorLog.append("The ReportMailingJobWritePlatformServiceImpl.generateReportOutputStream method threw an Exception: ").append(e)
.append(" ---------- ");
}
}
private void updateReportMailingJobAfterJobExecution(final ReportMailingJob reportMailingJob, final StringBuilder errorLog,
final LocalDateTime jobStartDateTime) {
final String recurrence = reportMailingJob.getRecurrence();
final LocalDateTime nextRunDateTime = reportMailingJob.getNextRunDateTime();
ReportMailingJobPreviousRunStatus reportMailingJobPreviousRunStatus = ReportMailingJobPreviousRunStatus.SUCCESS;
reportMailingJob.updatePreviousRunErrorLog(null);
if (errorLog != null && errorLog.length() > 0) {
reportMailingJobPreviousRunStatus = ReportMailingJobPreviousRunStatus.ERROR;
reportMailingJob.updatePreviousRunErrorLog(errorLog.toString());
}
reportMailingJob.increaseNumberOfRunsByOne();
reportMailingJob.updatePreviousRunStatus(reportMailingJobPreviousRunStatus.getValue());
reportMailingJob.updatePreviousRunDateTime(reportMailingJob.getNextRunDateTime());
if (StringUtils.isEmpty(recurrence)) {
reportMailingJob.deactivate();
reportMailingJob.updateNextRunDateTime(null);
} else if (nextRunDateTime != null) {
final LocalDateTime nextRecurringDateTime = createNextRecurringDateTime(recurrence, nextRunDateTime);
reportMailingJob.updateNextRunDateTime(nextRecurringDateTime);
}
reportMailingJobRepository.save(reportMailingJob);
createReportMailingJobRunHistroryAfterJobExecution(reportMailingJob, errorLog, jobStartDateTime,
reportMailingJobPreviousRunStatus.getValue());
}
private void sendReportFileToEmailRecipients(final ReportMailingJob reportMailingJob, final String fileName,
final ByteArrayOutputStream byteArrayOutputStream, final StringBuilder errorLog) {
final Set<String> emailRecipients = this.reportMailingJobValidator.validateEmailRecipients(reportMailingJob.getEmailRecipients());
try {
final File file = new File(fileName);
final FileOutputStream outputStream = new FileOutputStream(file);
byteArrayOutputStream.writeTo(outputStream);
for (String emailRecipient : emailRecipients) {
final ReportMailingJobEmailData reportMailingJobEmailData = new ReportMailingJobEmailData(emailRecipient,
reportMailingJob.getEmailMessage(), reportMailingJob.getEmailSubject(), file);
reportMailingJobEmailService.sendEmailWithAttachment(reportMailingJobEmailData);
}
outputStream.close();
} catch (IOException e) {
errorLog.append("The ReportMailingJobWritePlatformServiceImpl.sendReportFileToEmailRecipients method threw an IOException "
+ "exception: ").append(e).append(" ---------- ");
}
}
private LocalDateTime createNextRecurringDateTime(final String recurrencePattern, final LocalDateTime startDateTime) {
LocalDateTime nextRecurringDateTime = null;
if (StringUtils.isNotBlank(recurrencePattern) && startDateTime != null) {
final LocalDate nextDayLocalDate = startDateTime.plus(Duration.ofDays(1)).toLocalDate();
final LocalDate nextRecurringLocalDate = CalendarUtils.getNextRecurringDate(recurrencePattern, startDateTime.toLocalDate(),
nextDayLocalDate);
final String nextDateTimeString = nextRecurringLocalDate + " " + startDateTime.getHour() + ":" + startDateTime.getMinute() + ":"
+ startDateTime.getSecond();
final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_FORMAT);
nextRecurringDateTime = LocalDateTime.parse(nextDateTimeString, dateTimeFormatter);
}
return nextRecurringDateTime;
}
private void createReportMailingJobRunHistroryAfterJobExecution(final ReportMailingJob reportMailingJob, final StringBuilder errorLog,
final LocalDateTime jobStartDateTime, final String jobRunStatus) {
final LocalDateTime jobEndDateTime = DateUtils.getLocalDateTimeOfTenant();
final String errorLogToString = (errorLog != null) ? errorLog.toString() : null;
final ReportMailingJobRunHistory reportMailingJobRunHistory = ReportMailingJobRunHistory.newInstance(reportMailingJob,
jobStartDateTime, jobEndDateTime, jobRunStatus, null, errorLogToString);
reportMailingJobRunHistoryRepository.save(reportMailingJobRunHistory);
}
}
@@ -0,0 +1,63 @@
/**
* 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.campaigns.jobs.getdeliveryreportsfromsmsgateway;
import org.apache.fineract.infrastructure.campaigns.helper.SmsConfigUtils;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository;
import org.apache.fineract.infrastructure.sms.service.SmsReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GetDeliveryReportsFromSmsGatewayConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private SmsMessageRepository smsMessageRepository;
@Autowired
private SmsReadPlatformService smsReadPlatformService;
@Autowired
private SmsConfigUtils smsConfigUtils;
@Bean
protected Step getDeliveryReportsFromSmsGatewayStep() {
return steps.get(JobName.GET_DELIVERY_REPORTS_FROM_SMS_GATEWAY.name()).tasklet(getDeliveryReportsFromSmsGatewayTasklet()).build();
}
@Bean
public Job getDeliveryReportsFromSmsGatewayJob() {
return jobs.get(JobName.GET_DELIVERY_REPORTS_FROM_SMS_GATEWAY.name()).start(getDeliveryReportsFromSmsGatewayStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public GetDeliveryReportsFromSmsGatewayTasklet getDeliveryReportsFromSmsGatewayTasklet() {
return new GetDeliveryReportsFromSmsGatewayTasklet(smsReadPlatformService, smsConfigUtils, smsMessageRepository);
}
}
@@ -0,0 +1,112 @@
/**
* 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.campaigns.jobs.getdeliveryreportsfromsmsgateway;
import com.google.gson.Gson;
import java.net.URI;
import java.util.Collection;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.fineract.infrastructure.campaigns.helper.SmsConfigUtils;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.sms.data.SmsMessageDeliveryReportData;
import org.apache.fineract.infrastructure.sms.domain.SmsMessage;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageStatusType;
import org.apache.fineract.infrastructure.sms.service.SmsReadPlatformService;
import org.springframework.batch.core.StepContribution;
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.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
@Slf4j
@RequiredArgsConstructor
public class GetDeliveryReportsFromSmsGatewayTasklet implements Tasklet {
private final SmsReadPlatformService smsReadPlatformService;
private final SmsConfigUtils smsConfigUtils;
private final SmsMessageRepository smsMessageRepository;
private final RestTemplate restTemplate = new RestTemplate();
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
int page = 0;
int totalRecords;
Integer limit = 200;
do {
Page<Long> smsMessageInternalIds = smsReadPlatformService.retrieveAllWaitingForDeliveryReport(limit);
try {
if (!CollectionUtils.isEmpty(smsMessageInternalIds.getPageItems())) {
Map<String, Object> hostConfig = smsConfigUtils.getMessageGateWayRequestURI("sms/report",
new Gson().toJson(smsMessageInternalIds.getPageItems()));
URI uri = (URI) hostConfig.get("uri");
HttpEntity<?> entity = (HttpEntity<?>) hostConfig.get("entity");
ResponseEntity<Collection<SmsMessageDeliveryReportData>> responseOne = restTemplate.exchange(uri, HttpMethod.POST,
entity, new ParameterizedTypeReference<>() {});
Collection<SmsMessageDeliveryReportData> smsMessageDeliveryReportDataCollection = responseOne.getBody();
if (!CollectionUtils.isEmpty(smsMessageDeliveryReportDataCollection)) {
for (SmsMessageDeliveryReportData smsMessageDeliveryReportData : smsMessageDeliveryReportDataCollection) {
Integer deliveryStatus = smsMessageDeliveryReportData.getDeliveryStatus();
if (!smsMessageDeliveryReportData.getHasError() && deliveryStatus != 100) {
SmsMessage smsMessage = smsMessageRepository.findById(smsMessageDeliveryReportData.getId()).orElse(null);
Integer statusType = switch (deliveryStatus) {
case 0 -> SmsMessageStatusType.INVALID.getValue();
case 150 -> SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue();
case 200 -> SmsMessageStatusType.SENT.getValue();
case 300 -> SmsMessageStatusType.DELIVERED.getValue();
case 400 -> SmsMessageStatusType.FAILED.getValue();
default -> smsMessage.getStatusType();
};
boolean statusChanged = !statusType.equals(smsMessage.getStatusType());
smsMessage.setStatusType(statusType);
smsMessage.setExternalId(smsMessageDeliveryReportData.getExternalId());
smsMessageRepository.save(smsMessage);
if (statusChanged) {
log.info("Status of SMS message id: {} successfully changed to {}", smsMessage.getId(), statusType);
}
}
}
}
if (!CollectionUtils.isEmpty(smsMessageDeliveryReportDataCollection)) {
log.info("{} delivery report(s) successfully received from the intermediate gateway - sms",
smsMessageDeliveryReportDataCollection.size());
}
}
}
catch (Exception e) {
log.error("Error occurred.", e);
}
page++;
totalRecords = smsMessageInternalIds.getTotalFilteredRecords();
} while (page < totalRecords);
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,63 @@
/**
* 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.campaigns.jobs.sendmessagetosmsgateway;
import org.apache.fineract.infrastructure.campaigns.helper.SmsConfigUtils;
import org.apache.fineract.infrastructure.gcm.service.NotificationSenderService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SendMessageToSmsGatewayConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private SmsMessageRepository smsMessageRepository;
@Autowired
private NotificationSenderService notificationSenderService;
@Autowired
private SmsConfigUtils smsConfigUtils;
@Bean
protected Step sendMessageToSmsGatewayStep() {
return steps.get(JobName.SEND_MESSAGES_TO_SMS_GATEWAY.name()).tasklet(sendMessageToSmsGatewayTasklet()).build();
}
@Bean
public Job sendMessageToSmsGatewayJob() {
return jobs.get(JobName.SEND_MESSAGES_TO_SMS_GATEWAY.name()).start(sendMessageToSmsGatewayStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public SendMessageToSmsGatewayTasklet sendMessageToSmsGatewayTasklet() {
return new SendMessageToSmsGatewayTasklet(smsMessageRepository, notificationSenderService, smsConfigUtils);
}
}
@@ -0,0 +1,154 @@
/**
* 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.campaigns.jobs.sendmessagetosmsgateway;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.fineract.infrastructure.campaigns.helper.SmsConfigUtils;
import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignConstants;
import org.apache.fineract.infrastructure.campaigns.sms.exception.ConnectionFailureException;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.gcm.service.NotificationSenderService;
import org.apache.fineract.infrastructure.sms.data.SmsMessageApiQueueResourceData;
import org.apache.fineract.infrastructure.sms.domain.SmsMessage;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageStatusType;
import org.springframework.batch.core.StepContribution;
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.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
@Slf4j
@RequiredArgsConstructor
public class SendMessageToSmsGatewayTasklet implements Tasklet {
private final SmsMessageRepository smsMessageRepository;
private ExecutorService genericExecutorService;
private final NotificationSenderService notificationSenderService;
private final SmsConfigUtils smsConfigUtils;
private final RestTemplate restTemplate = new RestTemplate();
@PostConstruct
public void initializeExecutorService() {
genericExecutorService = Executors.newSingleThreadExecutor();
}
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
int pageLimit = 200;
int page = 0;
int totalRecords;
do {
PageRequest pageRequest = PageRequest.of(0, pageLimit);
org.springframework.data.domain.Page<SmsMessage> pendingMessages = smsMessageRepository
.findByStatusType(SmsMessageStatusType.PENDING.getValue(), pageRequest);
List<SmsMessage> toSaveMessages = new ArrayList<>();
List<SmsMessage> toSendNotificationMessages = new ArrayList<>();
try {
if (!CollectionUtils.isEmpty(pendingMessages.getContent())) {
final String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
Iterator<SmsMessage> pendingMessageIterator = pendingMessages.iterator();
Collection<SmsMessageApiQueueResourceData> apiQueueResourceDataCollection = new ArrayList<>();
while (pendingMessageIterator.hasNext()) {
SmsMessage smsData = pendingMessageIterator.next();
if (smsData.isNotification()) {
smsData.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue());
toSendNotificationMessages.add(smsData);
} else {
SmsMessageApiQueueResourceData apiQueueResourceData = SmsMessageApiQueueResourceData.instance(smsData.getId(),
tenantIdentifier, null, null, smsData.getMobileNo(), smsData.getMessage(),
smsData.getSmsCampaign().getProviderId());
apiQueueResourceDataCollection.add(apiQueueResourceData);
smsData.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue());
toSaveMessages.add(smsData);
}
}
if (!toSaveMessages.isEmpty()) {
smsMessageRepository.saveAll(toSaveMessages);
smsMessageRepository.flush();
genericExecutorService.execute(new SmsTask(ThreadLocalContextUtil.getTenant(), apiQueueResourceDataCollection));
}
if (!toSendNotificationMessages.isEmpty()) {
notificationSenderService.sendNotification(toSendNotificationMessages);
}
}
} catch (Exception e) {
throw new ConnectionFailureException(SmsCampaignConstants.SMS, e);
}
page++;
totalRecords = pendingMessages.getTotalPages();
} while (page < totalRecords);
return RepeatStatus.FINISHED;
}
class SmsTask implements Runnable, ApplicationListener<ContextClosedEvent> {
private final FineractPlatformTenant tenant;
private final Collection<SmsMessageApiQueueResourceData> apiQueueResourceDatas;
SmsTask(final FineractPlatformTenant tenant, final Collection<SmsMessageApiQueueResourceData> apiQueueResourceDatas) {
this.tenant = tenant;
this.apiQueueResourceDatas = apiQueueResourceDatas;
}
@Override
public void run() {
ThreadLocalContextUtil.setTenant(tenant);
connectAndSendToIntermediateServer(apiQueueResourceDatas);
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
genericExecutorService.shutdown();
log.info("Shutting down the ExecutorService");
}
}
private void connectAndSendToIntermediateServer(Collection<SmsMessageApiQueueResourceData> apiQueueResourceDatas) {
Map<String, Object> hostConfig = smsConfigUtils.getMessageGateWayRequestURI("sms",
SmsMessageApiQueueResourceData.toJsonString(apiQueueResourceDatas));
URI uri = (URI) hostConfig.get("uri");
HttpEntity<?> entity = (HttpEntity<?>) hostConfig.get("entity");
ResponseEntity<String> responseOne = restTemplate.exchange(uri, HttpMethod.POST, entity, new ParameterizedTypeReference<>() {});
if (!responseOne.getStatusCode().equals(HttpStatus.ACCEPTED)) {
log.debug("{}", responseOne.getStatusCode().name());
throw new ConnectionFailureException(SmsCampaignConstants.SMS);
}
}
}
@@ -16,16 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.service;
package org.apache.fineract.infrastructure.campaigns.jobs.updateemailoutboundwithcampaignmessage;
import java.util.Map;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
public class EmailParamMappingException extends RuntimeException {
public interface LoanSchedularService {
void applyChargeForOverdueLoans() throws JobExecutionException;
void recalculateInterest() throws JobExecutionException;
void recalculateInterest(@SuppressWarnings("unused") Map<String, String> jobParameters);
public EmailParamMappingException(Exception e) {
super(e);
}
}
@@ -0,0 +1,71 @@
/**
* 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.campaigns.jobs.updateemailoutboundwithcampaignmessage;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaignRepository;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageRepository;
import org.apache.fineract.infrastructure.campaigns.email.service.EmailCampaignReadPlatformService;
import org.apache.fineract.infrastructure.campaigns.email.service.EmailCampaignWritePlatformService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UpdateEmailOutboundWithCampaignMessageConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private EmailCampaignReadPlatformService emailCampaignReadPlatformService;
@Autowired
private EmailCampaignRepository emailCampaignRepository;
@Autowired
private ClientRepositoryWrapper clientRepositoryWrapper;
@Autowired
private EmailMessageRepository emailMessageRepository;
@Autowired
private EmailCampaignWritePlatformService emailCampaignWritePlatformService;
@Bean
protected Step updateEmailOutboundWithCampaignMessageStep() {
return steps.get(JobName.UPDATE_EMAIL_OUTBOUND_WITH_CAMPAIGN_MESSAGE.name())
.tasklet(updateEmailOutboundWithCampaignMessageTasklet()).build();
}
@Bean
public Job updateEmailOutboundWithCampaignMessageJob() {
return jobs.get(JobName.UPDATE_EMAIL_OUTBOUND_WITH_CAMPAIGN_MESSAGE.name()).start(updateEmailOutboundWithCampaignMessageStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public UpdateEmailOutboundWithCampaignMessageTasklet updateEmailOutboundWithCampaignMessageTasklet() {
return new UpdateEmailOutboundWithCampaignMessageTasklet(emailCampaignReadPlatformService, emailCampaignRepository,
clientRepositoryWrapper, emailMessageRepository, emailCampaignWritePlatformService);
}
}
@@ -0,0 +1,150 @@
/**
* 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.campaigns.jobs.updateemailoutboundwithcampaignmessage;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.campaigns.email.data.EmailCampaignData;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaign;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailCampaignRepository;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessage;
import org.apache.fineract.infrastructure.campaigns.email.domain.EmailMessageRepository;
import org.apache.fineract.infrastructure.campaigns.email.exception.EmailCampaignNotFound;
import org.apache.fineract.infrastructure.campaigns.email.service.EmailCampaignReadPlatformService;
import org.apache.fineract.infrastructure.campaigns.email.service.EmailCampaignWritePlatformService;
import org.apache.fineract.infrastructure.campaigns.jobs.TenantDateTimeUtil;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class UpdateEmailOutboundWithCampaignMessageTasklet implements Tasklet {
private final EmailCampaignReadPlatformService emailCampaignReadPlatformService;
private final EmailCampaignRepository emailCampaignRepository;
private final ClientRepositoryWrapper clientRepositoryWrapper;
private final EmailMessageRepository emailMessageRepository;
private final EmailCampaignWritePlatformService emailCampaignWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final Collection<EmailCampaignData> emailCampaignDataCollection = emailCampaignReadPlatformService
.retrieveAllScheduleActiveCampaign();
if (emailCampaignDataCollection != null) {
for (EmailCampaignData emailCampaignData : emailCampaignDataCollection) {
LocalDateTime tenantDateNow = TenantDateTimeUtil.tenantDateTime();
LocalDateTime nextTriggerDate = emailCampaignData.getNextTriggerDate().toLocalDateTime();
log.info("tenant time {} trigger time {}", tenantDateNow, nextTriggerDate);
if (nextTriggerDate.isBefore(tenantDateNow)) {
insertDirectCampaignIntoEmailOutboundTable(emailCampaignData.getParamValue(), emailCampaignData.getEmailSubject(),
emailCampaignData.getMessage(), emailCampaignData.getCampaignName(), emailCampaignData.getId());
updateTriggerDates(emailCampaignData.getId());
}
}
}
return RepeatStatus.FINISHED;
}
private void insertDirectCampaignIntoEmailOutboundTable(final String emailParams, final String emailSubject,
final String messageTemplate, final String campaignName, final Long campaignId) {
try {
HashMap<String, String> campaignParams = new ObjectMapper().readValue(emailParams, new TypeReference<>() {});
HashMap<String, String> queryParamForRunReport = new ObjectMapper().readValue(emailParams, new TypeReference<>() {});
List<HashMap<String, Object>> runReportObject = emailCampaignWritePlatformService
.getRunReportByServiceImpl(campaignParams.get("reportName"), queryParamForRunReport);
if (runReportObject != null) {
for (HashMap<String, Object> entry : runReportObject) {
String message = compileEmailTemplate(messageTemplate, campaignName, entry);
Integer clientId = (Integer) entry.get("id");
EmailCampaign emailCampaign = emailCampaignRepository.findById(campaignId).orElse(null);
Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(clientId.longValue());
String emailAddress = client.emailAddress();
if (emailAddress != null && isValidEmail(emailAddress)) {
EmailMessage emailMessage = EmailMessage.pendingEmail(null, client, null, emailCampaign, emailSubject, message,
emailAddress, campaignName);
emailMessageRepository.save(emailMessage);
}
}
}
} catch (IOException e) {
throw new EmailParamMappingException(e);
}
}
private void updateTriggerDates(Long campaignId) {
final EmailCampaign emailCampaign = emailCampaignRepository.findById(campaignId)
.orElseThrow(() -> new EmailCampaignNotFound(campaignId));
LocalDateTime nextTriggerDate = emailCampaign.getNextTriggerDate();
emailCampaign.setLastTriggerDate(nextTriggerDate);
LocalDateTime newTriggerDateWithTime = CalendarUtils.getNextRecurringDate(emailCampaign.getRecurrence(),
emailCampaign.getNextTriggerDate(), nextTriggerDate);
if (newTriggerDateWithTime.isBefore(DateUtils.getLocalDateTimeOfTenant())) {
newTriggerDateWithTime = CalendarUtils.getNextRecurringDate(emailCampaign.getRecurrence(), emailCampaign.getNextTriggerDate(),
DateUtils.getLocalDateTimeOfTenant());
}
emailCampaign.setNextTriggerDate(newTriggerDateWithTime);
emailCampaignRepository.saveAndFlush(emailCampaign);
}
private String compileEmailTemplate(final String textMessageTemplate, final String campaignName,
final Map<String, Object> emailParams) {
final MustacheFactory mf = new DefaultMustacheFactory();
final Mustache mustache = mf.compile(new StringReader(textMessageTemplate), campaignName);
final StringWriter stringWriter = new StringWriter();
mustache.execute(stringWriter, emailParams);
return stringWriter.toString();
}
public static boolean isValidEmail(String email) {
boolean isValid = true;
try {
InternetAddress emailO = new InternetAddress(email);
emailO.validate();
} catch (AddressException ex) {
isValid = false;
}
return isValid;
}
}
@@ -0,0 +1,62 @@
/**
* 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.campaigns.jobs.updatesmsoutboundwithcampaignmessage;
import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaignRepository;
import org.apache.fineract.infrastructure.campaigns.sms.service.SmsCampaignWritePlatformService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UpdateSmsOutboundWithCampaignMessageConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private SmsCampaignRepository smsCampaignRepository;
@Autowired
private SmsCampaignWritePlatformService smsCampaignWritePlatformService;
@Bean
protected Step updateSmsOutboundWithCampaignMessageStep() {
return steps.get(JobName.UPDATE_SMS_OUTBOUND_WITH_CAMPAIGN_MESSAGE.name()).tasklet(updateSmsOutboundWithCampaignMessageTasklet())
.build();
}
@Bean
public Job updateSmsOutboundWithCampaignMessageJob() {
return jobs.get(JobName.UPDATE_SMS_OUTBOUND_WITH_CAMPAIGN_MESSAGE.name()).start(updateSmsOutboundWithCampaignMessageStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public UpdateSmsOutboundWithCampaignMessageTasklet updateSmsOutboundWithCampaignMessageTasklet() {
return new UpdateSmsOutboundWithCampaignMessageTasklet(smsCampaignRepository, smsCampaignWritePlatformService);
}
}
@@ -0,0 +1,83 @@
/**
* 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.campaigns.jobs.updatesmsoutboundwithcampaignmessage;
import java.time.LocalDateTime;
import java.util.Collection;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.fineract.infrastructure.campaigns.jobs.TenantDateTimeUtil;
import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignStatus;
import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignTriggerType;
import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaign;
import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaignRepository;
import org.apache.fineract.infrastructure.campaigns.sms.exception.SmsCampaignNotFound;
import org.apache.fineract.infrastructure.campaigns.sms.service.SmsCampaignWritePlatformService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class UpdateSmsOutboundWithCampaignMessageTasklet implements Tasklet {
private final SmsCampaignRepository smsCampaignRepository;
private final SmsCampaignWritePlatformService smsCampaignWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final Collection<SmsCampaign> smsCampaignDataCollection = smsCampaignRepository
.findByTriggerTypeAndStatus(SmsCampaignTriggerType.SCHEDULE.getValue(), SmsCampaignStatus.ACTIVE.getValue());
if (!CollectionUtils.isEmpty(smsCampaignDataCollection)) {
for (SmsCampaign smsCampaign : smsCampaignDataCollection) {
LocalDateTime tenantDateNow = TenantDateTimeUtil.tenantDateTime();
LocalDateTime nextTriggerDate = smsCampaign.getNextTriggerDate();
log.info("tenant time {} trigger time {} {}", tenantDateNow, nextTriggerDate,
JobName.UPDATE_SMS_OUTBOUND_WITH_CAMPAIGN_MESSAGE.name());
if (nextTriggerDate.isBefore(tenantDateNow)) {
smsCampaignWritePlatformService.insertDirectCampaignIntoSmsOutboundTable(smsCampaign);
updateTriggerDates(smsCampaign.getId());
}
}
}
return RepeatStatus.FINISHED;
}
private void updateTriggerDates(Long campaignId) {
final SmsCampaign smsCampaign = this.smsCampaignRepository.findById(campaignId)
.orElseThrow(() -> new SmsCampaignNotFound(campaignId));
LocalDateTime nextTriggerDate = smsCampaign.getNextTriggerDate();
smsCampaign.setLastTriggerDate(nextTriggerDate);
LocalDateTime nextRuntime = CalendarUtils.getNextRecurringDate(smsCampaign.getRecurrence(), smsCampaign.getNextTriggerDate(),
nextTriggerDate);
if (nextRuntime.isBefore(DateUtils.getLocalDateTimeOfTenant())) {
nextRuntime = CalendarUtils.getNextRecurringDate(smsCampaign.getRecurrence(), smsCampaign.getNextTriggerDate(),
DateUtils.getLocalDateTimeOfTenant());
}
smsCampaign.setNextTriggerDate(nextRuntime);
this.smsCampaignRepository.saveAndFlush(smsCampaign);
}
}
@@ -38,6 +38,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
@@ -57,13 +58,13 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Path("/smscampaigns")
@Component
@Scope("singleton")
@RequiredArgsConstructor
public class SmsCampaignApiResource {
private final PlatformSecurityContext platformSecurityContext;
@@ -74,81 +75,80 @@ public class SmsCampaignApiResource {
private final FromJsonHelper fromJsonHelper;
private final DefaultToApiJsonSerializer<CampaignPreviewData> previewCampaignMessageDefaultToApiJsonSerializer;
private final SmsCampaignWritePlatformService smsCampaignWritePlatformService;
private final String resourceNameForPermissions = "SMS_CAMPAIGN";
private final PlatformSecurityContext context;
@Autowired
public SmsCampaignApiResource(final PlatformSecurityContext platformSecurityContext,
final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService,
final DefaultToApiJsonSerializer<SmsCampaignData> toApiJsonSerializer,
final SmsCampaignReadPlatformService smsCampaignReadPlatformService, final ApiRequestParameterHelper apiRequestParameterHelper,
final FromJsonHelper fromJsonHelper,
final DefaultToApiJsonSerializer<CampaignPreviewData> previewCampaignMessageDefaultToApiJsonSerializer,
final PlatformSecurityContext context, final SmsCampaignWritePlatformService smsCampaignWritePlatformService) {
this.platformSecurityContext = platformSecurityContext;
this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
this.toApiJsonSerializer = toApiJsonSerializer;
this.smsCampaignReadPlatformService = smsCampaignReadPlatformService;
this.apiRequestParameterHelper = apiRequestParameterHelper;
this.fromJsonHelper = fromJsonHelper;
this.previewCampaignMessageDefaultToApiJsonSerializer = previewCampaignMessageDefaultToApiJsonSerializer;
this.context = context;
this.smsCampaignWritePlatformService = smsCampaignWritePlatformService;
}
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "SMS_CAMPAIGN";
@GET
@Path("template")
@Operation(summary = "Retrieve a SMS Campaign", description = "Example Requests:\n" + "\n" + "smscampaigns/1\n" + "\n" + "\n"
+ "smscampaigns/1?template=true\n" + "\n" + "\n" + "smscampaigns/template")
@Operation(summary = "Retrieve a SMS Campaign", description = """
Example Requests:
smscampaigns/1
smscampaigns/1?template=true
smscampaigns/template""")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SmsCampaignData.class)))
public String template(@Context final UriInfo uriInfo) {
this.platformSecurityContext.authenticatedUser().validateHasReadPermission(SmsCampaignConstants.RESOURCE_NAME);
final SmsCampaignData smsCampaignData = this.smsCampaignReadPlatformService.retrieveTemplate(CampaignType.SMS.name());
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, smsCampaignData);
platformSecurityContext.authenticatedUser().validateHasReadPermission(SmsCampaignConstants.RESOURCE_NAME);
final SmsCampaignData smsCampaignData = smsCampaignReadPlatformService.retrieveTemplate(CampaignType.SMS.name());
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, smsCampaignData);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create a SMS Campaign", description = "Mandatory Fields\n"
+ "campaignName, campaignType, triggerType, providerId, runReportId, message\n" + "\n"
+ "Mandatory Fields for Cash based on selected report id\n" + "paramValue in json format")
@Operation(summary = "Create a SMS Campaign", description = """
Mandatory Fields
campaignName, campaignType, triggerType, providerId, runReportId, message
Mandatory Fields for Cash based on selected report id
paramValue in json format""")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = CommandWrapper.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = CommandProcessingResult.class)))
public String createCampaign(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.platformSecurityContext.authenticatedUser();
platformSecurityContext.authenticatedUser();
final CommandWrapper commandRequest = new CommandWrapperBuilder().createSmsCampaign().withJson(apiRequestBodyAsJson).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return toApiJsonSerializer.serialize(result);
}
@GET
@Path("{resourceId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a SMS Campaign", description = "Example Requests:\n" + "\n" + "smscampaigns/1\n")
@Operation(summary = "Retrieve a SMS Campaign", description = """
Example Requests:
smscampaigns/1
""")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SmsCampaignData.class)))
public String retrieveCampaign(@PathParam("resourceId") final Long resourceId, @Context final UriInfo uriInfo) {
this.platformSecurityContext.authenticatedUser().validateHasReadPermission(SmsCampaignConstants.RESOURCE_NAME);
SmsCampaignData smsCampaignData = this.smsCampaignReadPlatformService.retrieveOne(resourceId);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, smsCampaignData);
platformSecurityContext.authenticatedUser().validateHasReadPermission(SmsCampaignConstants.RESOURCE_NAME);
SmsCampaignData smsCampaignData = smsCampaignReadPlatformService.retrieveOne(resourceId);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, smsCampaignData);
}
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List SMS Campaigns", description = "Example Requests:\n" + "\n" + "smscampaigns")
@Operation(summary = "List SMS Campaigns", description = """
Example Requests:
smscampaigns""")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = SmsCampaignData.class)))
public String retrieveAllEmails(@QueryParam("sqlSearch") final String sqlSearch, @QueryParam("offset") final Integer offset,
@QueryParam("limit") final Integer limit, @QueryParam("orderBy") final String orderBy,
@QueryParam("sortOrder") final String sortOrder, @Context final UriInfo uriInfo) {
this.platformSecurityContext.authenticatedUser().validateHasReadPermission(SmsCampaignConstants.RESOURCE_NAME);
platformSecurityContext.authenticatedUser().validateHasReadPermission(SmsCampaignConstants.RESOURCE_NAME);
final SearchParameters searchParameters = SearchParameters.forSMSCampaign(sqlSearch, offset, limit, orderBy, sortOrder);
Page<SmsCampaignData> smsCampaignDataCollection = this.smsCampaignReadPlatformService.retrieveAll(searchParameters);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, smsCampaignDataCollection);
Page<SmsCampaignData> smsCampaignDataCollection = smsCampaignReadPlatformService.retrieveAll(searchParameters);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, smsCampaignDataCollection);
}
@PUT
@@ -162,8 +162,8 @@ public class SmsCampaignApiResource {
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateSmsCampaign(campaignId).withJson(apiRequestBodyAsJson)
.build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return toApiJsonSerializer.serialize(result);
}
@POST
@@ -176,18 +176,18 @@ public class SmsCampaignApiResource {
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandProcessingResult result = null;
CommandWrapper commandRequest = null;
CommandWrapper commandRequest;
if (is(commandParam, "activate")) {
commandRequest = builder.activateSmsCampaign(campaignId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "close")) {
commandRequest = builder.closeSmsCampaign(campaignId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "reactivate")) {
commandRequest = builder.reactivateSmsCampaign(campaignId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
return this.toApiJsonSerializer.serialize(result);
return toApiJsonSerializer.serialize(result);
}
@POST
@@ -195,14 +195,14 @@ public class SmsCampaignApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String preview(final String apiRequestBodyAsJson, @Context final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
CampaignPreviewData campaignMessage = null;
final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson);
final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper);
campaignMessage = this.smsCampaignWritePlatformService.previewMessage(query);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.previewCampaignMessageDefaultToApiJsonSerializer.serialize(settings, campaignMessage, new HashSet<String>());
CampaignPreviewData campaignMessage;
final JsonElement parsedQuery = fromJsonHelper.parse(apiRequestBodyAsJson);
final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, fromJsonHelper);
campaignMessage = smsCampaignWritePlatformService.previewMessage(query);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return previewCampaignMessageDefaultToApiJsonSerializer.serialize(settings, campaignMessage, new HashSet<>());
}
@@ -212,8 +212,8 @@ public class SmsCampaignApiResource {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = CommandProcessingResult.class)))
public String delete(@PathParam("campaignId") final Long campaignId) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteSmsCampaign(campaignId).build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return toApiJsonSerializer.serialize(result);
}
private boolean is(final String commandParam, final String commandValue) {
@@ -167,7 +167,7 @@ public class SmsCampaign extends AbstractPersistableCustom {
LocalDateTime recurrenceStartDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
if (SmsCampaignTriggerType.fromInt(triggerType.intValue()).isSchedule()) {
final Locale locale = command.extractLocale();
String dateTimeFormat = null;
String dateTimeFormat;
if (command.hasParameter(SmsCampaignValidator.dateTimeFormat)) {
dateTimeFormat = command.stringValueOfParameterNamed(SmsCampaignValidator.dateTimeFormat);
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(dateTimeFormat).withLocale(locale);
@@ -24,7 +24,6 @@ import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaign;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
@@ -43,14 +42,14 @@ public interface SmsCampaignWritePlatformService {
CommandProcessingResult reactivateSmsCampaign(Long campaignId, JsonCommand command);
void insertDirectCampaignIntoSmsOutboundTable(SmsCampaign smsCampaign);
void insertDirectCampaignIntoSmsOutboundTable(Loan loan, SmsCampaign smsCampaign);
String compileSmsTemplate(String textMessageTemplate, String campaignName, Map<String, Object> smsParams);
CampaignPreviewData previewMessage(JsonQuery query);
void storeTemplateMessageIntoSmsOutBoundTable() throws JobExecutionException;
void insertDirectCampaignIntoSmsOutboundTable(Client client, SmsCampaign smsCampaign);
void insertDirectCampaignIntoSmsOutboundTable(SavingsAccount savingsAccount, SmsCampaign smsCampaign);
@@ -41,8 +41,8 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignStatus;
import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignTriggerType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.campaigns.sms.data.CampaignPreviewData;
import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaign;
import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaignRepository;
@@ -66,10 +66,6 @@ import org.apache.fineract.infrastructure.dataqueries.domain.ReportRepository;
import org.apache.fineract.infrastructure.dataqueries.exception.ReportNotFoundException;
import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService;
import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService;
import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationRepositoryWrapper;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.sms.domain.SmsMessage;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository;
@@ -84,19 +80,16 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTypeException;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.useradministration.domain.AppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
@RequiredArgsConstructor
public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWritePlatformService {
private static final Logger LOG = LoggerFactory.getLogger(SmsCampaignWritePlatformServiceJpaImpl.class);
private final PlatformSecurityContext context;
private final SmsCampaignRepository smsCampaignRepository;
@@ -108,32 +101,9 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
private final ReadReportingService readReportingService;
private final GenericDataService genericDataService;
private final FromJsonHelper fromJsonHelper;
private final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository;
private final SmsMessageScheduledJobService smsMessageScheduledJobService;
@Autowired
public SmsCampaignWritePlatformServiceJpaImpl(final PlatformSecurityContext context, final SmsCampaignRepository smsCampaignRepository,
final SmsCampaignValidator smsCampaignValidator, final ReportRepository reportRepository,
final SmsMessageRepository smsMessageRepository, final ClientRepositoryWrapper clientRepositoryWrapper,
final ReadReportingService readReportingService, final GenericDataService genericDataService,
final FromJsonHelper fromJsonHelper, final GroupRepository groupRepository,
final SmsMessageScheduledJobService smsMessageScheduledJobService,
final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository) {
this.context = context;
this.smsCampaignRepository = smsCampaignRepository;
this.smsCampaignValidator = smsCampaignValidator;
this.reportRepository = reportRepository;
this.smsMessageRepository = smsMessageRepository;
this.clientRepositoryWrapper = clientRepositoryWrapper;
this.readReportingService = readReportingService;
this.genericDataService = genericDataService;
this.fromJsonHelper = fromJsonHelper;
this.groupRepository = groupRepository;
this.smsMessageScheduledJobService = smsMessageScheduledJobService;
this.deviceRegistrationRepository = deviceRegistrationRepository;
}
@Transactional
@Override
public CommandProcessingResult create(JsonCommand command) {
@@ -150,10 +120,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
}
this.smsCampaignRepository.saveAndFlush(smsCampaign);
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(smsCampaign.getId()) //
.build();
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(smsCampaign.getId()).build();
}
@Transactional
@@ -210,45 +177,41 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
smsCampaign.delete();
this.smsCampaignRepository.saveAndFlush(smsCampaign);
return new CommandProcessingResultBuilder() //
.withEntityId(smsCampaign.getId()) //
.build();
return new CommandProcessingResultBuilder().withEntityId(smsCampaign.getId()).build();
}
private void insertDirectCampaignIntoSmsOutboundTable(SmsCampaign smsCampaign) {
@Override
public void insertDirectCampaignIntoSmsOutboundTable(SmsCampaign smsCampaign) {
try {
HashMap<String, String> campaignParams = new ObjectMapper().readValue(smsCampaign.getParamValue(),
new TypeReference<HashMap<String, String>>() {});
HashMap<String, String> campaignParams = new ObjectMapper().readValue(smsCampaign.getParamValue(), new TypeReference<>() {});
HashMap<String, String> queryParamForRunReport = new ObjectMapper().readValue(smsCampaign.getParamValue(),
new TypeReference<HashMap<String, String>>() {});
new TypeReference<>() {});
List<HashMap<String, Object>> runReportObject = this.getRunReportByServiceImpl(campaignParams.get("reportName"),
List<HashMap<String, Object>> runReportObject = getRunReportByServiceImpl(campaignParams.get("reportName"),
queryParamForRunReport);
if (runReportObject != null) {
for (HashMap<String, Object> entry : runReportObject) {
String textMessage = this.compileSmsTemplate(smsCampaign.getMessage(), smsCampaign.getCampaignName(), entry);
String textMessage = compileSmsTemplate(smsCampaign.getMessage(), smsCampaign.getCampaignName(), entry);
Integer clientId = (Integer) entry.get("id");
Object mobileNo = entry.get("mobileNo");
Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId.longValue());
if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) {
// String countryCode =
// this.smsReadPlatformService.retrieveCountryCode(client.getOffice().getId()).getCountryCode();
Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(clientId.longValue());
if (smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) {
String mobileNumber = null;
if (mobileNo != null) {
mobileNumber = mobileNo.toString();
}
SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage, mobileNumber, smsCampaign,
smsCampaign.isNotification());
this.smsMessageRepository.save(smsMessage);
smsMessageRepository.save(smsMessage);
}
}
}
} catch (final IOException e) {
LOG.error("Error occured.", e);
log.error("Error occurred.", e);
}
}
@@ -309,10 +272,8 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
}
}
}
} catch (final IOException e) {
LOG.error("Error occured.", e);
} catch (final RuntimeException e) {
LOG.error("Error occured.", e);
} catch (final IOException | RuntimeException e) {
log.error("Error occured.", e);
}
}
@@ -353,10 +314,8 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
}
}
}
} catch (final IOException e) {
LOG.error("Error occured.", e);
} catch (final RuntimeException e) {
LOG.error("Error occured.", e);
} catch (final IOException | RuntimeException e) {
log.error("Error occured.", e);
}
}
@@ -396,52 +355,19 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
}
}
}
} catch (final IOException e) {
LOG.error("Error occured.", e);
} catch (final RuntimeException e) {
LOG.error("Error occured.", e);
} catch (final IOException | RuntimeException e) {
log.error("Error occured.", e);
}
}
private void updateTriggerDates(Long campaignId) {
final SmsCampaign smsCampaign = this.smsCampaignRepository.findById(campaignId)
.orElseThrow(() -> new SmsCampaignNotFound(campaignId));
LocalDateTime nextTriggerDate = smsCampaign.getNextTriggerDate();
smsCampaign.setLastTriggerDate(nextTriggerDate);
// calculate new trigger date and insert into next trigger date
/**
* next run time has to be in the future if not calculate a new future date
*/
LocalDateTime nextRuntime = CalendarUtils.getNextRecurringDate(smsCampaign.getRecurrence(), smsCampaign.getNextTriggerDate(),
nextTriggerDate);
if (nextRuntime.isBefore(DateUtils.getLocalDateTimeOfTenant())) { // means
// next
// run
// time is
// in the
// past
// calculate
// a new
// future
// date
nextRuntime = CalendarUtils.getNextRecurringDate(smsCampaign.getRecurrence(), smsCampaign.getNextTriggerDate(),
DateUtils.getLocalDateTimeOfTenant());
}
smsCampaign.setNextTriggerDate(nextRuntime);
this.smsCampaignRepository.saveAndFlush(smsCampaign);
}
@Transactional
@Override
public CommandProcessingResult activateSmsCampaign(Long campaignId, JsonCommand command) {
final AppUser currentUser = this.context.authenticatedUser();
final AppUser currentUser = context.authenticatedUser();
this.smsCampaignValidator.validateActivation(command.json());
final SmsCampaign smsCampaign = this.smsCampaignRepository.findById(campaignId)
.orElseThrow(() -> new SmsCampaignNotFound(campaignId));
final SmsCampaign smsCampaign = smsCampaignRepository.findById(campaignId).orElseThrow(() -> new SmsCampaignNotFound(campaignId));
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
@@ -449,7 +375,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
smsCampaign.activate(currentUser, fmt, activationDate);
this.smsCampaignRepository.saveAndFlush(smsCampaign);
smsCampaignRepository.saveAndFlush(smsCampaign);
if (smsCampaign.isDirect()) {
insertDirectCampaignIntoSmsOutboundTable(smsCampaign);
@@ -475,10 +401,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
* if campaign is direct insert campaign message into sms outbound table else if its a schedule create a job
* process for it
*/
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(smsCampaign.getId()) //
.build();
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(smsCampaign.getId()).build();
}
@Transactional
@@ -529,7 +452,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
final String response = this.genericDataService.generateJsonFromGenericResultsetData(results);
resultList = new ObjectMapper().readValue(response, new TypeReference<List<HashMap<String, Object>>>() {});
} catch (JsonParseException e) {
LOG.info("Conversion of report query results to JSON failed", e);
log.info("Conversion of report query results to JSON failed", e);
return resultList;
}
// loop changes array date to string date
@@ -628,30 +551,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
}
this.smsCampaignRepository.saveAndFlush(smsCampaign);
return new CommandProcessingResultBuilder() //
.withEntityId(smsCampaign.getId()) //
.build();
}
@Override
@CronTarget(jobName = JobName.UPDATE_SMS_OUTBOUND_WITH_CAMPAIGN_MESSAGE)
public void storeTemplateMessageIntoSmsOutBoundTable() throws JobExecutionException {
final Collection<SmsCampaign> smsCampaignDataCollection = this.smsCampaignRepository
.findByTriggerTypeAndStatus(SmsCampaignTriggerType.SCHEDULE.getValue(), SmsCampaignStatus.ACTIVE.getValue());
if (smsCampaignDataCollection != null) {
for (SmsCampaign smsCampaign : smsCampaignDataCollection) {
LocalDateTime tenantDateNow = tenantDateTime();
LocalDateTime nextTriggerDate = smsCampaign.getNextTriggerDate();
LOG.info("tenant time {} trigger time {} {}", tenantDateNow, nextTriggerDate,
JobName.UPDATE_SMS_OUTBOUND_WITH_CAMPAIGN_MESSAGE.name());
if (nextTriggerDate.isBefore(tenantDateNow)) {
insertDirectCampaignIntoSmsOutboundTable(smsCampaign);
this.updateTriggerDates(smsCampaign.getId());
}
}
}
return new CommandProcessingResultBuilder().withEntityId(smsCampaign.getId()).build();
}
private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause) {
@@ -672,23 +572,4 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP
}
return today;
}
// private String formatDestinationPhoneNumber(String phoneNumber, String
// countryCallingCode) {
// StringBuilder formatedPhoneNumber = new StringBuilder("+");
//
// try {
// Long phoneNumberToLong = Long.parseLong(phoneNumber);
// Long countryCallingCodeToLong = Long.parseLong(countryCallingCode);
// formatedPhoneNumber.append(Long.toString(countryCallingCodeToLong));
// formatedPhoneNumber.append(Long.toString(phoneNumberToLong));
// }
//
// catch (Exception e) {
// LOG.error("Invalid phone number or country calling code, must contain
// only numbers", e);
// }
//
// return formatedPhoneNumber.toString();
// }
}
@@ -23,7 +23,6 @@ import java.util.Map;
import org.apache.fineract.infrastructure.core.auditing.JpaAuditingHandlerRegistrar;
import org.apache.fineract.infrastructure.core.domain.AuditorAwareImpl;
import org.apache.fineract.infrastructure.core.persistence.DatabaseSelectingPersistenceUnitPostProcessor;
import org.apache.fineract.infrastructure.core.persistence.ExtendedJpaTransactionManager;
import org.apache.fineract.infrastructure.core.service.RoutingDataSource;
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
import org.eclipse.persistence.config.PersistenceUnitProperties;
@@ -31,7 +30,6 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilderCustomizer;
import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
@@ -102,14 +100,6 @@ public class JPAConfig extends JpaBaseConfiguration {
return new AuditorAwareImpl();
}
@Override
@Bean
public PlatformTransactionManager transactionManager(ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
ExtendedJpaTransactionManager transactionManager = new ExtendedJpaTransactionManager();
transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
return transactionManager;
}
@Bean
public TransactionTemplate txTemplate(PlatformTransactionManager transactionManager) {
TransactionTemplate tt = new TransactionTemplate();
@@ -22,6 +22,7 @@ import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Service;
@@ -36,6 +37,7 @@ import org.springframework.stereotype.Service;
* of this request.
*/
@Service(value = "routingDataSource")
@Primary
public class RoutingDataSource extends AbstractDataSource {
@Autowired
@@ -20,10 +20,10 @@ package org.apache.fineract.infrastructure.gcm.service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.configuration.service.ExternalServicesPropertiesReadPlatformService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.gcm.GcmConstants;
@@ -38,29 +38,20 @@ import org.apache.fineract.infrastructure.gcm.domain.Sender;
import org.apache.fineract.infrastructure.sms.domain.SmsMessage;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageStatusType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class NotificationSenderService {
private final DeviceRegistrationRepositoryWrapper deviceRegistrationRepositoryWrapper;
private final SmsMessageRepository smsMessageRepository;
private ExternalServicesPropertiesReadPlatformService propertiesReadPlatformService;
@Autowired
public NotificationSenderService(final DeviceRegistrationRepositoryWrapper deviceRegistrationRepositoryWrapper,
final SmsMessageRepository smsMessageRepository,
final ExternalServicesPropertiesReadPlatformService propertiesReadPlatformService) {
this.deviceRegistrationRepositoryWrapper = deviceRegistrationRepositoryWrapper;
this.smsMessageRepository = smsMessageRepository;
this.propertiesReadPlatformService = propertiesReadPlatformService;
}
private final ExternalServicesPropertiesReadPlatformService propertiesReadPlatformService;
public void sendNotification(List<SmsMessage> smsMessages) {
Map<Long, List<SmsMessage>> notificationByEachClient = getNotificationListByClient(smsMessages);
for (Map.Entry<Long, List<SmsMessage>> entry : notificationByEachClient.entrySet()) {
this.sendNotifiaction(entry.getKey(), entry.getValue());
sendNotification(entry.getKey(), entry.getValue());
}
}
@@ -72,7 +63,7 @@ public class NotificationSenderService {
if (notificationByEachClient.containsKey(clientId)) {
notificationByEachClient.get(clientId).add(smsMessage);
} else {
List<SmsMessage> msgList = new ArrayList<>(Arrays.asList(smsMessage));
List<SmsMessage> msgList = new ArrayList<>(List.of(smsMessage));
notificationByEachClient.put(clientId, msgList);
}
@@ -81,10 +72,10 @@ public class NotificationSenderService {
return notificationByEachClient;
}
public void sendNotifiaction(Long clientId, List<SmsMessage> smsList) {
public void sendNotification(Long clientId, List<SmsMessage> smsList) {
DeviceRegistration deviceRegistration = this.deviceRegistrationRepositoryWrapper.findDeviceRegistrationByClientId(clientId);
NotificationConfigurationData notificationConfigurationData = this.propertiesReadPlatformService.getNotificationConfiguration();
DeviceRegistration deviceRegistration = deviceRegistrationRepositoryWrapper.findDeviceRegistrationByClientId(clientId);
NotificationConfigurationData notificationConfigurationData = propertiesReadPlatformService.getNotificationConfiguration();
String registrationId = null;
if (deviceRegistration != null) {
registrationId = deviceRegistration.getRegistrationId();
@@ -93,18 +84,10 @@ public class NotificationSenderService {
try {
Notification notification = new Notification.Builder(GcmConstants.defaultIcon).title(GcmConstants.title)
.body(smsMessage.getMessage()).build();
Message.Builder b = new Message.Builder();
b.notification(notification);
b.dryRun(false);
b.contentAvailable(true);
b.timeToLive(GcmConstants.TIME_TO_LIVE);
b.priority(Priority.HIGH);
b.delayWhileIdle(true);
Message msg = b.build();
Sender s = new Sender(notificationConfigurationData.getServerKey(), notificationConfigurationData.getFcmEndPoint());
Result res;
res = s.send(msg, registrationId, 3);
Message message = new Message.Builder().notification(notification).dryRun(false).contentAvailable(true)
.timeToLive(GcmConstants.TIME_TO_LIVE).priority(Priority.HIGH).delayWhileIdle(true).build();
Sender sender = new Sender(notificationConfigurationData.getServerKey(), notificationConfigurationData.getFcmEndPoint());
Result res = sender.send(message, registrationId, 3);
if (res.getSuccess() != null && res.getSuccess() > 0) {
smsMessage.setStatusType(SmsMessageStatusType.SENT.getValue());
smsMessage.setDeliveredOnDate(DateUtils.getLocalDateTimeOfTenant());
@@ -116,7 +99,7 @@ public class NotificationSenderService {
}
}
this.smsMessageRepository.saveAll(smsList);
smsMessageRepository.saveAll(smsList);
}
@@ -0,0 +1,89 @@
/**
* 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;
import org.apache.fineract.infrastructure.core.persistence.ExtendedJpaTransactionManager;
import org.apache.fineract.infrastructure.core.service.RoutingDataSource;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.configuration.annotation.BatchConfigurer;
import org.springframework.batch.core.configuration.annotation.DefaultBatchConfigurer;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor;
import org.springframework.batch.core.launch.support.SimpleJobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@EnableBatchProcessing
public class ScheduledJobRunnerConfig {
@Bean
public PlatformTransactionManager transactionManager(ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
ExtendedJpaTransactionManager transactionManager = new ExtendedJpaTransactionManager();
transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager));
return transactionManager;
}
@Bean
public BatchConfigurer batchConfigurer(RoutingDataSource routingDataSource, PlatformTransactionManager platformTransactionManager) {
return new DefaultBatchConfigurer(routingDataSource) {
@Override
public PlatformTransactionManager getTransactionManager() {
return platformTransactionManager;
}
};
}
@Bean
public JobRepositoryFactoryBean jobRepositoryFactoryBean(RoutingDataSource routingDataSource,
PlatformTransactionManager transactionManager) throws Exception {
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(routingDataSource);
factory.setTransactionManager(transactionManager);
factory.setIsolationLevelForCreate("ISOLATION_REPEATABLE_READ");
factory.afterPropertiesSet();
return factory;
}
@Bean
public JobRepository jobRepository(JobRepositoryFactoryBean factory) throws Exception {
return factory.getObject();
}
@Bean
public SimpleJobLauncher jobLauncher(JobRepository jobRepository) throws Exception {
SimpleJobLauncher launcher = new SimpleJobLauncher();
launcher.setJobRepository(jobRepository);
launcher.afterPropertiesSet();
return launcher;
}
@Bean
public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) {
final JobRegistryBeanPostProcessor postProcessor = new JobRegistryBeanPostProcessor();
postProcessor.setJobRegistry(jobRegistry);
return postProcessor;
}
}
@@ -1,104 +0,0 @@
/**
* 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.annotation;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
/**
* Parser to find method which is marked with CronTargetMethod annotation
*/
public final class CronMethodParser {
private CronMethodParser() {
}
public static class ClassMethodNamesPair {
public String className;
public String methodName;
}
private static final String SEARCH_PACKAGE = "org.apache.fineract.";
private static final String CRON_ANNOTATION_ATTRIBUTE_NAME = "jobName";
private static final String RESOURCE_PATTERN = "**/*.class";
private static final Map<String, ClassMethodNamesPair> targetMethosMap = new HashMap<>();
private static final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
private static final MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
public static ClassMethodNamesPair findTargetMethodDetails(final String attributeValue) throws IOException {
if (!targetMethosMap.containsKey(attributeValue)) {
findAnnotationMethods(CronTarget.class, CRON_ANNOTATION_ATTRIBUTE_NAME);
}
return targetMethosMap.get(attributeValue);
}
/**
* method adds all the method names to map with annotation attribute value as key
*/
private static void findAnnotationMethods(final Class<? extends Annotation> annotationClass, final String attributeName)
throws IOException {
final String basePackagePath = ClassUtils
.convertClassNameToResourcePath(new StandardEnvironment().resolveRequiredPlaceholders(SEARCH_PACKAGE));
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + basePackagePath + "/" + RESOURCE_PATTERN;
packageSearchPath = packageSearchPath.replace("//", "/"); // else it
// doesn't
// work if
// *.class are
// in WAR!!
final Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
for (final Resource resource : resources) {
if (resource.isReadable()) {
final MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
final Set<MethodMetadata> metadataSet = metadataReader.getAnnotationMetadata()
.getAnnotatedMethods(annotationClass.getName());
if (metadataSet != null && metadataSet.size() > 0) {
for (final MethodMetadata metadata : metadataSet) {
final Map<String, Object> attributes = metadata.getAnnotationAttributes(annotationClass.getName());
final JobName attributeValue = (JobName) attributes.get(attributeName);
final String className = metadata.getDeclaringClassName();
final ClassMethodNamesPair pair = new ClassMethodNamesPair();
pair.className = className;
pair.methodName = metadata.getMethodName();
targetMethosMap.put(attributeValue.toString(), pair);
}
}
}
}
}
}
@@ -1,35 +0,0 @@
/**
* 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.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.fineract.infrastructure.jobs.service.JobName;
/**
* Annotation that marks a method to be picked while scheduling a cron jobs.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CronTarget {
JobName jobName();
}
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.jobs.data;
import java.util.Date;
import lombok.Getter;
public class JobDetailData {
@@ -34,7 +35,7 @@ public class JobDetailData {
@SuppressWarnings("unused")
private final String initializingError;
@SuppressWarnings("unused")
@Getter
private final String cronExpression;
@SuppressWarnings("unused")
@@ -18,38 +18,43 @@
*/
package org.apache.fineract.infrastructure.jobs.service;
import java.util.Arrays;
import java.util.Optional;
public enum JobName {
UPDATE_LOAN_ARREARS_AGEING("Update Loan Arrears Ageing"), APPLY_ANNUAL_FEE_FOR_SAVINGS(
"Apply Annual Fee For Savings"), APPLY_HOLIDAYS_TO_LOANS("Apply Holidays To Loans"), POST_INTEREST_FOR_SAVINGS(
"Post Interest For Savings"), TRANSFER_FEE_CHARGE_FOR_LOANS(
"Transfer Fee For Loans From Savings"), ACCOUNTING_RUNNING_BALANCE_UPDATE(
"Update Accounting Running Balances"), PAY_DUE_SAVINGS_CHARGES(
"Pay Due Savings Charges"), APPLY_CHARGE_TO_OVERDUE_LOAN_INSTALLMENT(
"Apply penalty to overdue loans"), EXECUTE_STANDING_INSTRUCTIONS(
"Execute Standing Instruction"), ADD_ACCRUAL_ENTRIES(
"Add Accrual Transactions"), UPDATE_NPA(
"Update Non Performing Assets"), UPDATE_DEPOSITS_ACCOUNT_MATURITY_DETAILS(
"Update Deposit Accounts Maturity details"), TRANSFER_INTEREST_TO_SAVINGS(
"Transfer Interest To Savings"), ADD_PERIODIC_ACCRUAL_ENTRIES(
"Add Periodic Accrual Transactions"), RECALCULATE_INTEREST_FOR_LOAN(
"Recalculate Interest For Loans"), GENERATE_RD_SCEHDULE(
"Generate Mandatory Savings Schedule"), GENERATE_LOANLOSS_PROVISIONING(
"Generate Loan Loss Provisioning"), POST_DIVIDENTS_FOR_SHARES(
"Post Dividends For Shares"), UPDATE_SAVINGS_DORMANT_ACCOUNTS(
"Update Savings Dormant Accounts"), ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS(
"Add Accrual Transactions For Loans With Income Posted As Transactions"), EXECUTE_REPORT_MAILING_JOBS(
"Execute Report Mailing Jobs"), UPDATE_SMS_OUTBOUND_WITH_CAMPAIGN_MESSAGE(
"Update SMS Outbound with Campaign Message"), SEND_MESSAGES_TO_SMS_GATEWAY(
"Send Messages to SMS Gateway"), GET_DELIVERY_REPORTS_FROM_SMS_GATEWAY(
"Get Delivery Reports from SMS Gateway"), GENERATE_ADHOCCLIENT_SCEHDULE(
"Generate AdhocClient Schedule"), UPDATE_EMAIL_OUTBOUND_WITH_CAMPAIGN_MESSAGE(
"Update Email Outbound with campaign message"), EXECUTE_EMAIL(
"Execute Email"), UPDATE_TRIAL_BALANCE_DETAILS(
"Update Trial Balance Details"), EXECUTE_DIRTY_JOBS(
"Execute All Dirty Jobs"), INCREASE_BUSINESS_DATE_BY_1_DAY(
"Increase Business Date by 1 day"), INCREASE_COB_DATE_BY_1_DAY(
"Increase COB Date by 1 day");
UPDATE_LOAN_ARREARS_AGEING("Update Loan Arrears Ageing"), //
APPLY_ANNUAL_FEE_FOR_SAVINGS("Apply Annual Fee For Savings"), //
APPLY_HOLIDAYS_TO_LOANS("Apply Holidays To Loans"), //
POST_INTEREST_FOR_SAVINGS("Post Interest For Savings"), //
TRANSFER_FEE_CHARGE_FOR_LOANS("Transfer Fee For Loans From Savings"), //
ACCOUNTING_RUNNING_BALANCE_UPDATE("Update Accounting Running Balances"), //
PAY_DUE_SAVINGS_CHARGES("Pay Due Savings Charges"), //
APPLY_CHARGE_TO_OVERDUE_LOAN_INSTALLMENT("Apply penalty to overdue loans"), //
EXECUTE_STANDING_INSTRUCTIONS("Execute Standing Instruction"), //
ADD_ACCRUAL_ENTRIES("Add Accrual Transactions"), //
UPDATE_NPA("Update Non Performing Assets"), //
UPDATE_DEPOSITS_ACCOUNT_MATURITY_DETAILS("Update Deposit Accounts Maturity details"), //
TRANSFER_INTEREST_TO_SAVINGS("Transfer Interest To Savings"), //
ADD_PERIODIC_ACCRUAL_ENTRIES("Add Periodic Accrual Transactions"), //
RECALCULATE_INTEREST_FOR_LOAN("Recalculate Interest For Loans"), //
GENERATE_RD_SCEHDULE("Generate Mandatory Savings Schedule"), //
GENERATE_LOANLOSS_PROVISIONING("Generate Loan Loss Provisioning"), //
POST_DIVIDENTS_FOR_SHARES("Post Dividends For Shares"), //
UPDATE_SAVINGS_DORMANT_ACCOUNTS("Update Savings Dormant Accounts"), //
ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS(
"Add Accrual Transactions For Loans With Income Posted As Transactions"), //
EXECUTE_REPORT_MAILING_JOBS("Execute Report Mailing Jobs"), //
UPDATE_SMS_OUTBOUND_WITH_CAMPAIGN_MESSAGE("Update SMS Outbound with Campaign Message"), //
SEND_MESSAGES_TO_SMS_GATEWAY("Send Messages to SMS Gateway"), //
GET_DELIVERY_REPORTS_FROM_SMS_GATEWAY("Get Delivery Reports from SMS Gateway"), //
GENERATE_ADHOC_CLIENT_SCHEDULE("Generate AdhocClient Schedule"), //
UPDATE_EMAIL_OUTBOUND_WITH_CAMPAIGN_MESSAGE("Update Email Outbound with campaign message"), //
EXECUTE_EMAIL("Execute Email"), //
UPDATE_TRIAL_BALANCE_DETAILS("Update Trial Balance Details"), //
EXECUTE_DIRTY_JOBS("Execute All Dirty Jobs"), //
INCREASE_BUSINESS_DATE_BY_1_DAY("Increase Business Date by 1 day"), //
INCREASE_COB_DATE_BY_1_DAY("Increase COB Date by 1 day");
private final String name;
@@ -57,6 +62,11 @@ public enum JobName {
this.name = name;
}
public static JobName getJobByName(String jobName) {
Optional<JobName> optionalJob = Arrays.stream(JobName.values()).filter(jn -> jobName.equals(jn.name)).findAny();
return optionalJob.orElseThrow(() -> new IllegalArgumentException("Job not found by name: " + jobName));
}
@Override
public String toString() {
return this.name;
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.infrastructure.jobs.service;
import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail;
public interface JobRegisterService {
void executeJob(Long jobId);
@@ -32,6 +34,8 @@ public interface JobRegisterService {
void stopScheduler(String name);
void scheduleJob(ScheduledJobDetail scheduledJobDetails);
void stopAllSchedulers();
}
@@ -20,27 +20,22 @@ package org.apache.fineract.infrastructure.jobs.service;
import com.google.common.base.Splitter;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.exception.PlatformInternalServerException;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.annotation.CronMethodParser;
import org.apache.fineract.infrastructure.jobs.annotation.CronMethodParser.ClassMethodNamesPair;
import org.apache.fineract.infrastructure.jobs.domain.JobParameter;
import org.apache.fineract.infrastructure.jobs.domain.JobParameterRepository;
import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail;
import org.apache.fineract.infrastructure.jobs.domain.SchedulerDetail;
import org.apache.fineract.infrastructure.jobs.exception.JobNodeIdMismatchingException;
import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException;
import org.apache.fineract.infrastructure.security.service.TenantDetailsService;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
@@ -49,10 +44,13 @@ import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
@@ -65,19 +63,12 @@ import org.springframework.stereotype.Service;
* ,{@link MethodInvokingJobDetailFactoryBean} and {@link CronTriggerFactoryBean}
*/
@Service
@Slf4j
public class JobRegisterServiceImpl implements JobRegisterService, ApplicationListener<ContextClosedEvent> {
private static final Logger LOG = LoggerFactory.getLogger(JobRegisterServiceImpl.class);
@Autowired
private ApplicationContext applicationContext;
@Autowired
private SchedularWritePlatformService schedularWritePlatformService;
@Autowired
private TenantDetailsService tenantDetailsService;
@Autowired
private SchedulerJobListener schedulerJobListener;
@@ -87,6 +78,9 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi
@Autowired
private JobParameterRepository jobParameterRepository;
@Autowired
private JobExplorer jobExplorer;
private final HashMap<String, Scheduler> schedulers = new HashMap<>(4);
// This cannot be injected as Autowired due to circular dependency
@@ -95,29 +89,13 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi
@Autowired
private FineractProperties fineractProperties;
@PostConstruct
public void loadAllJobs() {
// If the instance is not Batch Enabled will not load the Jobs
if (!fineractProperties.getMode().isBatchManagerEnabled()) {
return;
}
final List<FineractPlatformTenant> allTenants = this.tenantDetailsService.findAllTenants();
for (final FineractPlatformTenant tenant : allTenants) {
ThreadLocalContextUtil.setTenant(tenant);
final List<ScheduledJobDetail> scheduledJobDetails = this.schedularWritePlatformService
.retrieveAllJobs(fineractProperties.getNodeId());
for (final ScheduledJobDetail jobDetails : scheduledJobDetails) {
scheduleJob(jobDetails);
jobDetails.updateTriggerMisfired(false);
this.schedularWritePlatformService.saveOrUpdate(jobDetails);
}
final SchedulerDetail schedulerDetail = this.schedularWritePlatformService.retriveSchedulerDetail();
if (schedulerDetail.isResetSchedulerOnBootup()) {
schedulerDetail.updateSuspendedState(false);
this.schedularWritePlatformService.updateSchedulerDetail(schedulerDetail);
}
}
}
@Autowired
private JobLauncher jobLauncher;
@Autowired
private JobLocator jobLocator;
private static final String JOB_LAUNCHER_STARTER_METHOD_NAME = "run";
public void executeJob(final ScheduledJobDetail scheduledJobDetail, String triggerType) {
try {
@@ -127,25 +105,24 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi
}
jobDataMap.put(SchedulerServiceConstants.TRIGGER_TYPE_REFERENCE, triggerType);
jobDataMap.put(SchedulerServiceConstants.TENANT_IDENTIFIER, ThreadLocalContextUtil.getTenant().getTenantIdentifier());
final String key = scheduledJobDetail.getJobKey();
final JobKey jobKey = constructJobKey(key);
final String schedulerName = getSchedulerName(scheduledJobDetail);
final Scheduler scheduler = this.schedulers.get(schedulerName);
final JobDetail jobDetail = createJobDetail(scheduledJobDetail);
JobKey jobKey = jobDetail.getKey();
if (scheduler == null || !scheduler.checkExists(jobKey)) {
final JobDetail jobDetail = createJobDetail(scheduledJobDetail);
final String tempSchedulerName = "temp" + scheduledJobDetail.getId();
final Scheduler tempScheduler = createScheduler(tempSchedulerName, 1, schedulerJobListener, schedulerStopListener);
tempScheduler.addJob(jobDetail, true);
jobDataMap.put(SchedulerServiceConstants.SCHEDULER_NAME, tempSchedulerName);
this.schedulers.put(tempSchedulerName, tempScheduler);
tempScheduler.triggerJob(jobDetail.getKey(), jobDataMap);
schedulers.put(tempSchedulerName, tempScheduler);
tempScheduler.addJob(jobDetail, true);
tempScheduler.triggerJob(jobKey, jobDataMap);
} else {
scheduler.addJob(jobDetail, true);
scheduler.triggerJob(jobKey, jobDataMap);
}
} catch (final Exception e) {
final String msg = "Job execution failed for job with id:" + scheduledJobDetail.getId();
LOG.error("{}", msg, e);
log.error("{}", msg, e);
throw new PlatformInternalServerException("error.msg.sheduler.job.execution.failed", msg, scheduledJobDetail.getId(), e);
}
@@ -206,7 +183,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi
}
}
} catch (final SchedulerException e) {
LOG.error("Error occured.", e);
log.error("Error occured.", e);
}
}
jobDetail.updateTriggerMisfired(false);
@@ -264,7 +241,8 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi
this.stopAllSchedulers();
}
private void scheduleJob(final ScheduledJobDetail scheduledJobDetails) {
@Override
public void scheduleJob(final ScheduledJobDetail scheduledJobDetails) {
if (!scheduledJobDetails.isActiveSchedular()) {
scheduledJobDetails.updateNextRunTime(null);
scheduledJobDetails.updateCurrentlyRunningStatus(false);
@@ -282,7 +260,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi
scheduledJobDetails.updateNextRunTime(null);
final String stackTrace = getStackTraceAsString(throwable);
scheduledJobDetails.updateErrorLog(stackTrace);
LOG.error("Could not schedule job: {}", scheduledJobDetails.getJobName(), throwable);
log.error("Could not schedule job: {}", scheduledJobDetails.getJobName(), throwable);
}
scheduledJobDetails.updateCurrentlyRunningStatus(false);
}
@@ -293,7 +271,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi
try {
scheduler.shutdown();
} catch (final SchedulerException e) {
LOG.error("Error occured.", e);
log.error("Error occured.", e);
}
}
}
@@ -318,7 +296,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi
try {
scheduler.shutdown();
} catch (final SchedulerException e) {
LOG.error("Error occured.", e);
log.error("Error occurred.", e);
}
}
@@ -348,65 +326,36 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi
private JobDetail createJobDetail(final ScheduledJobDetail scheduledJobDetail) throws Exception {
final FineractPlatformTenant tenant = ThreadLocalContextUtil.getTenant();
final ClassMethodNamesPair jobDetails = CronMethodParser.findTargetMethodDetails(scheduledJobDetail.getJobName());
if (jobDetails == null) {
throw new IllegalArgumentException(
"Code has no @CronTarget with this job name (@see JobName); seems like DB/code are not in line: "
+ scheduledJobDetail.getJobName());
}
final Object targetObject = getBeanObject(Class.forName(jobDetails.className));
JobName jobName = JobName.getJobByName(scheduledJobDetail.getJobName());
Job job = jobLocator.getJob(jobName.name());
final MethodInvokingJobDetailFactoryBean jobDetailFactoryBean = new MethodInvokingJobDetailFactoryBean();
jobDetailFactoryBean.setName(scheduledJobDetail.getJobName() + "JobDetail" + tenant.getId());
jobDetailFactoryBean.setTargetObject(targetObject);
jobDetailFactoryBean.setTargetMethod(jobDetails.methodName);
jobDetailFactoryBean.setTargetObject(jobLauncher);
jobDetailFactoryBean.setTargetMethod(JOB_LAUNCHER_STARTER_METHOD_NAME);
jobDetailFactoryBean.setGroup(scheduledJobDetail.getGroupName());
jobDetailFactoryBean.setConcurrent(false);
Map<String, String> jobParameterMap = getJobParameter(scheduledJobDetail);
if (!jobParameterMap.isEmpty()) {
jobDetailFactoryBean.setArguments(jobParameterMap);
}
Map<String, org.springframework.batch.core.JobParameter> jobParameterMap = getJobParameter(scheduledJobDetail);
JobParameters jobParameters = new JobParametersBuilder(jobExplorer).getNextJobParameters(job)
.addJobParameters(new JobParameters(jobParameterMap)).toJobParameters();
jobDetailFactoryBean.setArguments(job, jobParameters);
jobDetailFactoryBean.afterPropertiesSet();
return jobDetailFactoryBean.getObject();
}
public Map<String, String> getJobParameter(ScheduledJobDetail scheduledJobDetail) {
public Map<String, org.springframework.batch.core.JobParameter> getJobParameter(ScheduledJobDetail scheduledJobDetail) {
List<JobParameter> jobParameterList = jobParameterRepository.findJobParametersByJobId(scheduledJobDetail.getId());
Map<String, String> jobParameterMap = new HashMap<>();
Map<String, org.springframework.batch.core.JobParameter> jobParameterMap = new HashMap<>();
for (JobParameter jobparameter : jobParameterList) {
jobParameterMap.put(jobparameter.getParameterName(), jobparameter.getParameterValue());
jobParameterMap.put(jobparameter.getParameterName(),
new org.springframework.batch.core.JobParameter(jobparameter.getParameterValue()));
}
return jobParameterMap;
}
private Object getBeanObject(final Class<?> classType) throws ClassNotFoundException {
final List<Class<?>> typesList = new ArrayList<>();
final Class<?>[] interfaceType = classType.getInterfaces();
if (interfaceType.length > 0) {
typesList.addAll(Arrays.asList(interfaceType));
} else {
Class<?> superclassType = classType;
while (!Object.class.getName().equals(superclassType.getSuperclass().getName())) {
superclassType = superclassType.getSuperclass();
}
typesList.add(superclassType);
}
final List<String> beanNames = new ArrayList<>();
for (final Class<?> clazz : typesList) {
beanNames.addAll(Arrays.asList(this.applicationContext.getBeanNamesForType(clazz)));
}
Object targetObject = null;
for (final String beanName : beanNames) {
final Object nextObject = this.applicationContext.getBean(beanName);
String targetObjName = nextObject.toString();
targetObjName = targetObjName.substring(0, targetObjName.lastIndexOf("@"));
if (classType.getName().equals(targetObjName)) {
targetObject = nextObject;
break;
}
}
return targetObject;
}
private Trigger createTrigger(final ScheduledJobDetail scheduledJobDetails, final JobDetail jobDetail) throws ParseException {
final FineractPlatformTenant tenant = ThreadLocalContextUtil.getTenant();
final CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
@@ -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;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail;
import org.apache.fineract.infrastructure.jobs.domain.SchedulerDetail;
import org.apache.fineract.infrastructure.security.service.TenantDetailsService;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class JobSchedulerServiceImpl implements ApplicationListener<ContextRefreshedEvent> {
private final FineractProperties fineractProperties;
private final SchedularWritePlatformService schedularWritePlatformService;
private final TenantDetailsService tenantDetailsService;
private final JobRegisterService jobRegisterService;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// If the instance is not Batch Enabled will not load the Jobs
if (!fineractProperties.getMode().isBatchManagerEnabled()) {
log.warn("Batch job scheduling is disabled since this instance is not a batch manager");
return;
}
final List<FineractPlatformTenant> allTenants = tenantDetailsService.findAllTenants();
for (final FineractPlatformTenant tenant : allTenants) {
ThreadLocalContextUtil.setTenant(tenant);
final List<ScheduledJobDetail> scheduledJobDetails = schedularWritePlatformService
.retrieveAllJobs(fineractProperties.getNodeId());
for (final ScheduledJobDetail jobDetails : scheduledJobDetails) {
jobRegisterService.scheduleJob(jobDetails);
jobDetails.updateTriggerMisfired(false);
schedularWritePlatformService.saveOrUpdate(jobDetails);
}
final SchedulerDetail schedulerDetail = schedularWritePlatformService.retriveSchedulerDetail();
if (schedulerDetail.isResetSchedulerOnBootup()) {
schedulerDetail.updateSuspendedState(false);
schedularWritePlatformService.updateSchedulerDetail(schedulerDetail);
}
}
log.info("Scheduling batch jobs has finished");
}
}
@@ -30,6 +30,8 @@ public interface SchedulerJobRunnerReadService {
JobDetailData retrieveOne(Long jobId);
JobDetailData retrieveOneByName(String jobName);
Page<JobDetailHistoryData> retrieveJobHistory(Long jobId, SearchParameters searchParameters);
boolean isUpdatesAllowed();
@@ -77,6 +77,17 @@ public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerRead
}
}
@Override
public JobDetailData retrieveOneByName(String jobName) {
try {
final JobDetailMapper detailMapper = new JobDetailMapper(sqlGenerator);
final String sql = detailMapper.schema() + " where job.name=?";
return this.jdbcTemplate.queryForObject(sql, detailMapper, new Object[] { jobName }); // NOSONAR
} catch (final EmptyResultDataAccessException e) {
throw new JobNotFoundException(jobName, e);
}
}
@Override
public Page<JobDetailHistoryData> retrieveJobHistory(final Long jobId, final SearchParameters searchParameters) {
if (!isJobExist(jobId)) {
@@ -142,9 +153,9 @@ public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerRead
JobDetailMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
sqlBuilder = new StringBuilder("select").append(
" job.id,job.display_name as displayName,job.next_run_time as nextRunTime,job.initializing_errorlog as initializingError,job.cron_expression as cronExpression,job.is_active as active,job.currently_running as currentlyRunning,")
.append(" runHistory.version,runHistory.start_time as lastRunStartTime,runHistory.end_time as lastRunEndTime,runHistory."
+ sqlGenerator.escape("status")
+ ",runHistory.error_message as jobRunErrorMessage,runHistory.trigger_type as triggerType,runHistory.error_log as jobRunErrorLog ")
.append(" runHistory.version,runHistory.start_time as lastRunStartTime,runHistory.end_time as lastRunEndTime,runHistory.")
.append(sqlGenerator.escape("status"))
.append(",runHistory.error_message as jobRunErrorMessage,runHistory.trigger_type as triggerType,runHistory.error_log as jobRunErrorLog ")
.append(" from job job left join job_run_history runHistory ON job.id=runHistory.job_id and job.previous_run_start_time=runHistory.start_time ");
}
@@ -36,6 +36,8 @@ import org.quartz.TriggerListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Component
@RequiredArgsConstructor
@@ -62,6 +64,7 @@ public class SchedulerTriggerListener implements TriggerListener {
@Override
@SuppressFBWarnings(value = {
"DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once")
@Transactional(isolation = Isolation.READ_COMMITTED)
public boolean vetoJobExecution(final Trigger trigger, final JobExecutionContext context) {
final String tenantIdentifier = trigger.getJobDataMap().getString(SchedulerServiceConstants.TENANT_IDENTIFIER);
final FineractPlatformTenant tenant = this.tenantDetailsService.loadTenantById(tenantIdentifier);
@@ -0,0 +1,50 @@
/**
* 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.executealldirtyjobs;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ExecuteAllDirtyJobsConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Bean
protected Step executeAllDirtyJobsStep(ExecuteAllDirtyJobsTasklet executeAllDirtyJobsTasklet) {
return steps.get(JobName.EXECUTE_DIRTY_JOBS.name()).tasklet(executeAllDirtyJobsTasklet).build();
}
@Bean
public Job executeAllDirtyJobsJob(ExecuteAllDirtyJobsTasklet executeAllDirtyJobsTasklet) {
return jobs.get(JobName.EXECUTE_DIRTY_JOBS.name()).start(executeAllDirtyJobsStep(executeAllDirtyJobsTasklet))
.incrementer(new RunIdIncrementer()).build();
}
}
@@ -0,0 +1,52 @@
/**
* 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.executealldirtyjobs;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail;
import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetailRepository;
import org.apache.fineract.infrastructure.jobs.service.JobRegisterService;
import org.springframework.batch.core.StepContribution;
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.stereotype.Component;
@RequiredArgsConstructor
@Component
public class ExecuteAllDirtyJobsTasklet implements Tasklet {
private final JobRegisterService jobRegisterService;
private final ScheduledJobDetailRepository scheduledJobDetailsRepository;
private final FineractProperties fineractProperties;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
List<ScheduledJobDetail> jobDetails = scheduledJobDetailsRepository.findAllMismatchedJobs(true);
for (ScheduledJobDetail scheduledJobDetail : jobDetails) {
if (scheduledJobDetail.getNodeId().toString().equals(fineractProperties.getNodeId())) {
jobRegisterService.executeJob(scheduledJobDetail.getId());
}
}
return RepeatStatus.FINISHED;
}
}
@@ -16,15 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.service;
package org.apache.fineract.infrastructure.jobs.service.increasedateby1day;
import java.util.Map;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
public interface SavingsSchedularService {
void postInterestForAccounts(Map<String, String> jobParameters) throws JobExecutionException;
void updateSavingsDormancyStatus() throws JobExecutionException;
public interface IncreaseDateBy1DayService {
void increaseDateByTypeByOneDay(BusinessDateType businessDateType);
}
@@ -0,0 +1,67 @@
/**
* 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.increasedateby1day;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.businessdate.data.BusinessDateData;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDate;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateRepository;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateWritePlatformService;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class IncreaseDateBy1DayServiceImpl implements IncreaseDateBy1DayService {
private final BusinessDateRepository businessDateRepository;
private final BusinessDateWritePlatformService businessDateWritePlatformService;
@Override
public void increaseDateByTypeByOneDay(BusinessDateType businessDateType) {
Map<String, Object> changes = new HashMap<>();
Optional<BusinessDate> businessDateEntity = businessDateRepository.findByType(businessDateType);
LocalDate businessDate = businessDateEntity.map(BusinessDate::getDate).orElse(DateUtils.getLocalDateOfTenant());
businessDate = businessDate.plusDays(1);
try {
BusinessDateData businessDateData = BusinessDateData.instance(businessDateType, businessDate);
businessDateWritePlatformService.adjustDate(businessDateData, changes);
} catch (final PlatformApiDataValidationException e) {
final List<ApiParameterError> errors = e.getErrors();
for (final ApiParameterError error : errors) {
log.error("Increasing {} by 1 day failed due to: {}", businessDateType.getDescription(), error.getDeveloperMessage());
}
} catch (final AbstractPlatformDomainRuleException e) {
log.error("Increasing {} by 1 day failed due to: {}", businessDateType.getDescription(), e.getDefaultUserMessage());
} catch (Exception e) {
log.error("Increasing {} by 1 day failed due to: {}", businessDateType.getDescription(), e.getMessage());
}
}
}
@@ -0,0 +1,57 @@
/**
* 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.increasedateby1day.increasebusinessdateby1day;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.jobs.service.increasedateby1day.IncreaseDateBy1DayService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class IncreaseBusinessDateBy1DayConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private IncreaseDateBy1DayService increaseDateBy1DayService;
@Bean
protected Step increaseBusinessDateBy1DayStep() {
return steps.get(JobName.INCREASE_BUSINESS_DATE_BY_1_DAY.name()).tasklet(increaseBusinessDateBy1DayTasklet()).build();
}
@Bean
public Job increaseBusinessDateBy1DayJob() {
return jobs.get(JobName.INCREASE_BUSINESS_DATE_BY_1_DAY.name()).start(increaseBusinessDateBy1DayStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public IncreaseBusinessDateBy1DayTasklet increaseBusinessDateBy1DayTasklet() {
return new IncreaseBusinessDateBy1DayTasklet(increaseDateBy1DayService);
}
}
@@ -0,0 +1,41 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.jobs.service.increasedateby1day.increasebusinessdateby1day;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.jobs.service.increasedateby1day.IncreaseDateBy1DayService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class IncreaseBusinessDateBy1DayTasklet implements Tasklet {
private final IncreaseDateBy1DayService increaseDateBy1DayService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
increaseDateBy1DayService.increaseDateByTypeByOneDay(BusinessDateType.BUSINESS_DATE);
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,57 @@
/**
* 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.increasedateby1day.increasecobdateby1day;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.jobs.service.increasedateby1day.IncreaseDateBy1DayService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class IncreaseCobDateBy1DayConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private IncreaseDateBy1DayService increaseDateBy1DayService;
@Bean
protected Step increaseCobDateBy1DayStep() {
return steps.get(JobName.INCREASE_COB_DATE_BY_1_DAY.name()).tasklet(increaseCobDateBy1DayTasklet()).build();
}
@Bean
public Job increaseCobDateBy1DayJob() {
return jobs.get(JobName.INCREASE_COB_DATE_BY_1_DAY.name()).start(increaseCobDateBy1DayStep()).incrementer(new RunIdIncrementer())
.build();
}
@Bean
public IncreaseCobDateBy1DayTasklet increaseCobDateBy1DayTasklet() {
return new IncreaseCobDateBy1DayTasklet(increaseDateBy1DayService);
}
}
@@ -0,0 +1,41 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.jobs.service.increasedateby1day.increasecobdateby1day;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.jobs.service.increasedateby1day.IncreaseDateBy1DayService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class IncreaseCobDateBy1DayTasklet implements Tasklet {
private final IncreaseDateBy1DayService increaseDateBy1DayService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
increaseDateBy1DayService.increaseDateByTypeByOneDay(BusinessDateType.COB_DATE);
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,63 @@
/**
* 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.updatenpa;
import org.apache.fineract.infrastructure.core.service.RoutingDataSourceServiceFactory;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UpdateNpaConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private RoutingDataSourceServiceFactory dataSourceServiceFactory;
@Autowired
private DatabaseTypeResolver databaseTypeResolver;
@Autowired
private DatabaseSpecificSQLGenerator sqlGenerator;
@Bean
protected Step updateNpaStep() {
return steps.get(JobName.UPDATE_NPA.name()).tasklet(updateNpaTasklet()).build();
}
@Bean
public Job updateNpaJob() {
return jobs.get(JobName.UPDATE_NPA.name()).start(updateNpaStep()).incrementer(new RunIdIncrementer()).build();
}
@Bean
public UpdateNpaTasklet updateNpaTasklet() {
return new UpdateNpaTasklet(dataSourceServiceFactory, databaseTypeResolver, sqlGenerator);
}
}
@@ -0,0 +1,81 @@
/**
* 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.updatenpa;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.RoutingDataSourceServiceFactory;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
import org.springframework.batch.core.StepContribution;
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.jdbc.core.JdbcTemplate;
@Slf4j
@RequiredArgsConstructor
public class UpdateNpaTasklet implements Tasklet {
private final RoutingDataSourceServiceFactory dataSourceServiceFactory;
private final DatabaseTypeResolver databaseTypeResolver;
private final DatabaseSpecificSQLGenerator sqlGenerator;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSourceServiceFactory.determineDataSourceService().retrieveDataSource());
final StringBuilder resetNPASqlBuilder = new StringBuilder();
resetNPASqlBuilder.append("update m_loan loan ");
String fromPart = " (SELECT loan2.* FROM m_loan loan2 left join m_loan_arrears_aging laa on laa.loan_id = loan2.id "
+ "inner join m_product_loan mpl on mpl.id = loan2.product_id and mpl.overdue_days_for_npa is not null "
+ "WHERE loan2.loan_status_id = 300 and mpl.account_moves_out_of_npa_only_on_arrears_completion = false"
+ " or (mpl.account_moves_out_of_npa_only_on_arrears_completion = true"
+ " and laa.overdue_since_date_derived is null)) sl";
String wherePart = " where loan.id = sl.id ";
if (databaseTypeResolver.isMySQL()) {
resetNPASqlBuilder.append(", ").append(fromPart).append(" set loan.is_npa = false").append(wherePart);
} else {
resetNPASqlBuilder.append("set is_npa = false").append(" FROM ").append(fromPart).append(wherePart);
}
jdbcTemplate.update(resetNPASqlBuilder.toString());
final StringBuilder updateSqlBuilder = new StringBuilder(900);
fromPart = " (select loan.id " + " FROM m_loan_arrears_aging laa" + " INNER JOIN m_loan loan on laa.loan_id = loan.id "
+ " INNER JOIN m_product_loan mpl on mpl.id = loan.product_id AND mpl.overdue_days_for_npa is not null "
+ "WHERE loan.loan_status_id = 300 and " + "laa.overdue_since_date_derived < "
+ sqlGenerator.subDate(sqlGenerator.currentBusinessDate(), "COALESCE(mpl.overdue_days_for_npa, 0)", "day")
+ " group by loan.id) as sl ";
wherePart = " where ml.id=sl.id ";
updateSqlBuilder.append("UPDATE m_loan as ml ");
if (databaseTypeResolver.isMySQL()) {
updateSqlBuilder.append(", ").append(fromPart).append(" SET ml.is_npa = true").append(wherePart);
} else {
updateSqlBuilder.append(" SET is_npa = true").append(" FROM ").append(fromPart).append(wherePart);
}
final int result = jdbcTemplate.update(updateSqlBuilder.toString());
log.info("{}: Records affected by updateNPA: {}", ThreadLocalContextUtil.getTenant().getName(), result);
return RepeatStatus.FINISHED;
}
}
@@ -20,7 +20,6 @@ package org.apache.fineract.infrastructure.reportmailingjob.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
public interface ReportMailingJobWritePlatformService {
@@ -29,6 +28,4 @@ public interface ReportMailingJobWritePlatformService {
CommandProcessingResult updateReportMailingJob(Long reportMailingJobId, JsonCommand jsonCommand);
CommandProcessingResult deleteReportMailingJob(Long reportMailingJobId);
void executeReportMailingJobs() throws JobExecutionException;
}
@@ -18,18 +18,9 @@
*/
package org.apache.fineract.infrastructure.reportmailingjob.service;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
@@ -38,29 +29,14 @@ import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityEx
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.dataqueries.domain.Report;
import org.apache.fineract.infrastructure.dataqueries.domain.ReportRepositoryWrapper;
import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService;
import org.apache.fineract.infrastructure.documentmanagement.contentrepository.FileSystemContentRepository;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.report.provider.ReportingProcessServiceProvider;
import org.apache.fineract.infrastructure.report.service.ReportingProcessService;
import org.apache.fineract.infrastructure.reportmailingjob.ReportMailingJobConstants;
import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobEmailAttachmentFileFormat;
import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobEmailData;
import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobPreviousRunStatus;
import org.apache.fineract.infrastructure.reportmailingjob.data.ReportMailingJobStretchyReportParamDateOption;
import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJob;
import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRepository;
import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRepositoryWrapper;
import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRunHistory;
import org.apache.fineract.infrastructure.reportmailingjob.domain.ReportMailingJobRunHistoryRepository;
import org.apache.fineract.infrastructure.reportmailingjob.util.ReportMailingJobDateUtil;
import org.apache.fineract.infrastructure.reportmailingjob.validation.ReportMailingJobValidator;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.useradministration.domain.AppUser;
import org.glassfish.jersey.internal.util.collection.MultivaluedStringMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -79,29 +55,17 @@ public class ReportMailingJobWritePlatformServiceImpl implements ReportMailingJo
private final ReportMailingJobRepositoryWrapper reportMailingJobRepositoryWrapper;
private final ReportMailingJobRepository reportMailingJobRepository;
private final PlatformSecurityContext platformSecurityContext;
private final ReportMailingJobEmailService reportMailingJobEmailService;
private final ReadReportingService readReportingService;
private final ReportingProcessServiceProvider reportingProcessServiceProvider;
private final ReportMailingJobRunHistoryRepository reportMailingJobRunHistoryRepository;
private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
@Autowired
public ReportMailingJobWritePlatformServiceImpl(final ReportRepositoryWrapper reportRepositoryWrapper,
final ReportMailingJobValidator reportMailingJobValidator,
final ReportMailingJobRepositoryWrapper reportMailingJobRepositoryWrapper,
final ReportMailingJobRepository reportMailingJobRepository, final PlatformSecurityContext platformSecurityContext,
final ReportMailingJobEmailService reportMailingJobEmailService, final ReadReportingService readReportingService,
final ReportMailingJobRunHistoryRepository reportMailingJobRunHistoryRepository,
final ReportingProcessServiceProvider reportingProcessServiceProvider) {
final PlatformSecurityContext platformSecurityContext) {
this.reportRepositoryWrapper = reportRepositoryWrapper;
this.reportMailingJobValidator = reportMailingJobValidator;
this.reportMailingJobRepositoryWrapper = reportMailingJobRepositoryWrapper;
this.reportMailingJobRepository = reportMailingJobRepositoryWrapper.getReportMailingJobRepository();
this.platformSecurityContext = platformSecurityContext;
this.reportMailingJobEmailService = reportMailingJobEmailService;
this.readReportingService = readReportingService;
this.reportMailingJobRunHistoryRepository = reportMailingJobRunHistoryRepository;
this.reportingProcessServiceProvider = reportingProcessServiceProvider;
}
@Override
@@ -242,114 +206,6 @@ public class ReportMailingJobWritePlatformServiceImpl implements ReportMailingJo
return new CommandProcessingResultBuilder().withEntityId(reportMailingJobId).build();
}
@Override
@CronTarget(jobName = JobName.EXECUTE_REPORT_MAILING_JOBS)
public void executeReportMailingJobs() throws JobExecutionException {
final Collection<ReportMailingJob> reportMailingJobCollection = this.reportMailingJobRepository
.findByIsActiveTrueAndIsDeletedFalse();
for (ReportMailingJob reportMailingJob : reportMailingJobCollection) {
final LocalDateTime localDateTimeOftenant = DateUtils.getLocalDateTimeOfTenant();
final LocalDateTime nextRunDateTime = reportMailingJob.getNextRunDateTime();
if (nextRunDateTime != null && nextRunDateTime.isBefore(localDateTimeOftenant)) {
// get the emailAttachmentFileFormat enum object
final ReportMailingJobEmailAttachmentFileFormat emailAttachmentFileFormat = ReportMailingJobEmailAttachmentFileFormat
.newInstance(reportMailingJob.getEmailAttachmentFileFormat());
if (emailAttachmentFileFormat != null && emailAttachmentFileFormat.isValid()) {
final Report stretchyReport = reportMailingJob.getStretchyReport();
final String reportName = (stretchyReport != null) ? stretchyReport.getReportName() : null;
final StringBuilder errorLog = new StringBuilder();
final Map<String, String> validateStretchyReportParamMap = this.reportMailingJobValidator
.validateStretchyReportParamMap(reportMailingJob.getStretchyReportParamMap());
MultivaluedMap<String, String> reportParams = new MultivaluedStringMap();
if (validateStretchyReportParamMap != null) {
Iterator<Map.Entry<String, String>> validateStretchyReportParamMapEntries = validateStretchyReportParamMap
.entrySet().iterator();
while (validateStretchyReportParamMapEntries.hasNext()) {
Map.Entry<String, String> validateStretchyReportParamMapEntry = validateStretchyReportParamMapEntries.next();
String key = validateStretchyReportParamMapEntry.getKey();
String value = validateStretchyReportParamMapEntry.getValue();
if (StringUtils.containsIgnoreCase(key, "date")) {
ReportMailingJobStretchyReportParamDateOption reportMailingJobStretchyReportParamDateOption = ReportMailingJobStretchyReportParamDateOption
.newInstance(value);
if (reportMailingJobStretchyReportParamDateOption.isValid()) {
value = ReportMailingJobDateUtil.getDateAsString(reportMailingJobStretchyReportParamDateOption);
}
}
reportParams.add(key, value);
}
}
// generate the report output stream, method in turn call
// another that sends the file to the email recipients
this.generateReportOutputStream(reportMailingJob, emailAttachmentFileFormat, reportParams, reportName, errorLog);
// update the previous run time, next run time, status,
// error log properties
this.updateReportMailingJobAfterJobExecution(reportMailingJob, errorLog, localDateTimeOftenant);
}
}
}
}
/**
* update the report mailing job entity after job execution
*
* @param reportMailingJob
* -- the report mailing job entity
* @param errorLog
* -- StringBuilder object containing the error log if any
* @param jobStartDateTime
* -- the start LocalDateTime of the job
*
**/
private void updateReportMailingJobAfterJobExecution(final ReportMailingJob reportMailingJob, final StringBuilder errorLog,
final LocalDateTime jobStartDateTime) {
final String recurrence = reportMailingJob.getRecurrence();
final LocalDateTime nextRunDateTime = reportMailingJob.getNextRunDateTime();
ReportMailingJobPreviousRunStatus reportMailingJobPreviousRunStatus = ReportMailingJobPreviousRunStatus.SUCCESS;
reportMailingJob.updatePreviousRunErrorLog(null);
if (errorLog != null && errorLog.length() > 0) {
reportMailingJobPreviousRunStatus = ReportMailingJobPreviousRunStatus.ERROR;
reportMailingJob.updatePreviousRunErrorLog(errorLog.toString());
}
reportMailingJob.increaseNumberOfRunsByOne();
reportMailingJob.updatePreviousRunStatus(reportMailingJobPreviousRunStatus.getValue());
reportMailingJob.updatePreviousRunDateTime(reportMailingJob.getNextRunDateTime());
// check if the job has a recurrence pattern, if not deactivate the job.
// The job will only run once
if (StringUtils.isEmpty(recurrence)) {
// deactivate job
reportMailingJob.deactivate();
// job will only run once, no next run time
reportMailingJob.updateNextRunDateTime(null);
} else if (nextRunDateTime != null) {
final LocalDateTime nextRecurringDateTime = this.createNextRecurringDateTime(recurrence, nextRunDateTime);
// finally update the next run date time property
reportMailingJob.updateNextRunDateTime(nextRecurringDateTime);
}
// save the ReportMailingJob entity
this.reportMailingJobRepository.save(reportMailingJob);
// create a new report mailing job run history entity
this.createReportMailingJobRunHistroryAfterJobExecution(reportMailingJob, errorLog, jobStartDateTime,
reportMailingJobPreviousRunStatus.getValue());
}
/**
* create the next recurring LocalDateTime from recurrence pattern, start LocalDateTime and current DateTime
*
@@ -369,29 +225,6 @@ public class ReportMailingJobWritePlatformServiceImpl implements ReportMailingJo
return nextRecurringDateTime;
}
/**
* create a new report mailing job run history entity after job execution
*
* @param reportMailingJob
* -- the report mailing job entity
* @param errorLog
* -- StringBuilder object containing the error log if any
* @param jobStartDateTime
* -- the start LocalDateTime of the job
* @param jobRunStatus
* -- the status of the job (success/error)
*
**/
private void createReportMailingJobRunHistroryAfterJobExecution(final ReportMailingJob reportMailingJob, final StringBuilder errorLog,
final LocalDateTime jobStartDateTime, final String jobRunStatus) {
final LocalDateTime jobEndDateTime = DateUtils.getLocalDateTimeOfTenant();
final String errorLogToString = (errorLog != null) ? errorLog.toString() : null;
final ReportMailingJobRunHistory reportMailingJobRunHistory = ReportMailingJobRunHistory.newInstance(reportMailingJob,
jobStartDateTime, jobEndDateTime, jobRunStatus, null, errorLogToString);
this.reportMailingJobRunHistoryRepository.save(reportMailingJobRunHistory);
}
/**
* Handle any SQL data integrity issue
*
@@ -415,92 +248,4 @@ public class ReportMailingJobWritePlatformServiceImpl implements ReportMailingJo
throw new PlatformDataIntegrityException("error.msg.charge.unknown.data.integrity.issue",
"Unknown data integrity issue with resource: " + realCause.getMessage());
}
/**
* generate the report output stream
*
* @param reportMailingJob
* @param emailAttachmentFileFormat
* @param reportParams
* @param reportName
* @param errorLog
* @return the error log StringBuilder object
*/
private StringBuilder generateReportOutputStream(final ReportMailingJob reportMailingJob,
final ReportMailingJobEmailAttachmentFileFormat emailAttachmentFileFormat, final MultivaluedMap<String, String> reportParams,
final String reportName, final StringBuilder errorLog) {
try {
final boolean isSelfServiceUserReport = false;
final String reportType = this.readReportingService.getReportType(reportName, isSelfServiceUserReport, false);
final ReportingProcessService reportingProcessService = this.reportingProcessServiceProvider
.findReportingProcessService(reportType);
if (reportingProcessService != null) {
final Response processReport = reportingProcessService.processRequest(reportName, reportParams);
final Object reponseObject = (processReport != null) ? processReport.getEntity() : null;
if (reponseObject != null && reponseObject.getClass().equals(ByteArrayOutputStream.class)) {
final ByteArrayOutputStream byteArrayOutputStream = ByteArrayOutputStream.class.cast(reponseObject);
final String fileLocation = FileSystemContentRepository.FINERACT_BASE_DIR + File.separator + "";
final String fileNameWithoutExtension = fileLocation + File.separator + reportName;
// check if file directory exists, if not create directory
if (!new File(fileLocation).isDirectory()) {
new File(fileLocation).mkdirs();
}
if ((byteArrayOutputStream == null) || byteArrayOutputStream.size() == 0) {
errorLog.append("Report processing failed, empty output stream created");
} else if ((errorLog != null && errorLog.length() == 0) && (byteArrayOutputStream.size() > 0)) {
final String fileName = fileNameWithoutExtension + "." + emailAttachmentFileFormat.getValue();
// send the file to email recipients
this.sendReportFileToEmailRecipients(reportMailingJob, fileName, byteArrayOutputStream, errorLog);
}
} else {
errorLog.append("Response object entity is not equal to ByteArrayOutputStream ---------- ");
}
} else {
errorLog.append(ReportingProcessServiceProvider.SERVICE_MISSING + reportType);
}
} catch (Exception e) {
errorLog.append("The ReportMailingJobWritePlatformServiceImpl.generateReportOutputStream method threw an Exception: " + e
+ " ---------- ");
}
return errorLog;
}
/**
* send report file to email recipients
*
* @param reportMailingJob
* @param fileName
* @param byteArrayOutputStream
* @param errorLog
*/
private void sendReportFileToEmailRecipients(final ReportMailingJob reportMailingJob, final String fileName,
final ByteArrayOutputStream byteArrayOutputStream, final StringBuilder errorLog) {
final Set<String> emailRecipients = this.reportMailingJobValidator.validateEmailRecipients(reportMailingJob.getEmailRecipients());
try {
final File file = new File(fileName);
final FileOutputStream outputStream = new FileOutputStream(file);
byteArrayOutputStream.writeTo(outputStream);
for (String emailRecipient : emailRecipients) {
final ReportMailingJobEmailData reportMailingJobEmailData = new ReportMailingJobEmailData(emailRecipient,
reportMailingJob.getEmailMessage(), reportMailingJob.getEmailSubject(), file);
this.reportMailingJobEmailService.sendEmailWithAttachment(reportMailingJobEmailData);
}
outputStream.close();
} catch (IOException e) {
errorLog.append("The ReportMailingJobWritePlatformServiceImpl.sendReportFileToEmailRecipients method threw an IOException "
+ "exception: " + e + " ---------- ");
}
}
}
@@ -33,6 +33,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.journalentry.api.DateParam;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
@@ -46,7 +47,6 @@ import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.infrastructure.sms.data.SmsData;
import org.apache.fineract.infrastructure.sms.service.SmsReadPlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@@ -56,6 +56,7 @@ import org.springframework.stereotype.Component;
@Component
@Scope("singleton")
@Tag(name = "SMS", description = "")
@RequiredArgsConstructor
public class SmsApiResource {
private final String resourceNameForPermissions = "SMS";
@@ -66,17 +67,6 @@ public class SmsApiResource {
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@Autowired
public SmsApiResource(final PlatformSecurityContext context, final SmsReadPlatformService readPlatformService,
final DefaultToApiJsonSerializer<SmsData> toApiJsonSerializer, final ApiRequestParameterHelper apiRequestParameterHelper,
final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService) {
this.context = context;
this.readPlatformService = readPlatformService;
this.toApiJsonSerializer = toApiJsonSerializer;
this.apiRequestParameterHelper = apiRequestParameterHelper;
this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
}
@GET
public String retrieveAll(@Context final UriInfo uriInfo) {
@@ -117,7 +107,7 @@ public class SmsApiResource {
@QueryParam("offset") final Integer offset, @QueryParam("limit") final Integer limit,
@QueryParam("orderBy") final String orderBy, @QueryParam("sortOrder") final String sortOrder) {
this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions);
context.authenticatedUser().validateHasReadPermission(resourceNameForPermissions);
final SearchParameters searchParameters = SearchParameters.forSMSCampaign(sqlSearch, offset, limit, orderBy, sortOrder);
@@ -130,11 +120,11 @@ public class SmsApiResource {
toDate = toDateParam.getDate("toDate", dateFormat, locale);
}
final Page<SmsData> smsMessages = this.readPlatformService.retrieveSmsByStatus(campaignId, searchParameters, status.intValue(),
fromDate, toDate);
final Page<SmsData> smsMessages = readPlatformService.retrieveSmsByStatus(campaignId, searchParameters, status.intValue(), fromDate,
toDate);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, smsMessages);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return toApiJsonSerializer.serialize(settings, smsMessages);
}
@PUT
@@ -28,11 +28,6 @@ import org.apache.fineract.infrastructure.sms.domain.SmsMessage;
**/
public interface SmsMessageScheduledJobService {
/**
* sends a batch of SMS messages to the SMS gateway
**/
void sendMessagesToGateway();
/**
* sends triggered batch SMS messages to SMS gateway
*
@@ -49,9 +44,4 @@ public interface SmsMessageScheduledJobService {
* the provider ID of the SMS gateway to be used
*/
void sendTriggeredMessage(Collection<SmsMessage> smsMessage, long providerId);
/**
* get delivery report from the SMS gateway
**/
void getDeliveryReports();
}
@@ -18,7 +18,6 @@
*/
package org.apache.fineract.infrastructure.sms.scheduler;
import com.google.gson.Gson;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
@@ -28,34 +27,27 @@ import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.campaigns.helper.SmsConfigUtils;
import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignConstants;
import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaign;
import org.apache.fineract.infrastructure.campaigns.sms.exception.ConnectionFailureException;
import org.apache.fineract.infrastructure.core.domain.FineractContext;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.gcm.service.NotificationSenderService;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.sms.data.SmsMessageApiQueueResourceData;
import org.apache.fineract.infrastructure.sms.data.SmsMessageDeliveryReportData;
import org.apache.fineract.infrastructure.sms.domain.SmsMessage;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository;
import org.apache.fineract.infrastructure.sms.domain.SmsMessageStatusType;
import org.apache.fineract.infrastructure.sms.service.SmsReadPlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
/**
@@ -63,89 +55,22 @@ import org.springframework.web.client.RestTemplate;
**/
@Service
@Slf4j
@RequiredArgsConstructor
public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJobService {
private final SmsMessageRepository smsMessageRepository;
private final SmsReadPlatformService smsReadPlatformService;
private final RestTemplate restTemplate = new RestTemplate();
private final SmsConfigUtils smsConfigUtils;
private final NotificationSenderService notificationSenderService;
private ExecutorService genericExecutorService;
private ExecutorService triggeredExecutorService;
/**
* SmsMessageScheduledJobServiceImpl constructor
**/
@Autowired
public SmsMessageScheduledJobServiceImpl(SmsMessageRepository smsMessageRepository, SmsReadPlatformService smsReadPlatformService,
final SmsConfigUtils smsConfigUtils, final NotificationSenderService notificationSenderService) {
this.smsMessageRepository = smsMessageRepository;
this.smsReadPlatformService = smsReadPlatformService;
this.smsConfigUtils = smsConfigUtils;
this.notificationSenderService = notificationSenderService;
}
@PostConstruct
public void initializeExecutorService() {
genericExecutorService = Executors.newSingleThreadExecutor();
triggeredExecutorService = Executors.newSingleThreadExecutor();
}
/**
* Send batches of SMS messages to the SMS gateway (or intermediate gateway)
**/
@Override
@Transactional
@CronTarget(jobName = JobName.SEND_MESSAGES_TO_SMS_GATEWAY)
public void sendMessagesToGateway() {
Integer pageLimit = 200;
Integer page = 0;
int totalRecords = 0;
do {
PageRequest pageRequest = PageRequest.of(0, pageLimit);
org.springframework.data.domain.Page<SmsMessage> pendingMessages = this.smsMessageRepository
.findByStatusType(SmsMessageStatusType.PENDING.getValue(), pageRequest);
List<SmsMessage> toSaveMessages = new ArrayList<>();
List<SmsMessage> toSendNotificationMessages = new ArrayList<>();
try {
if (pendingMessages.getContent().size() > 0) {
final String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
Iterator<SmsMessage> pendingMessageIterator = pendingMessages.iterator();
Collection<SmsMessageApiQueueResourceData> apiQueueResourceDatas = new ArrayList<>();
while (pendingMessageIterator.hasNext()) {
SmsMessage smsData = pendingMessageIterator.next();
if (smsData.isNotification()) {
smsData.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue());
toSendNotificationMessages.add(smsData);
} else {
SmsMessageApiQueueResourceData apiQueueResourceData = SmsMessageApiQueueResourceData.instance(smsData.getId(),
tenantIdentifier, null, null, smsData.getMobileNo(), smsData.getMessage(),
smsData.getSmsCampaign().getProviderId());
apiQueueResourceDatas.add(apiQueueResourceData);
smsData.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue());
toSaveMessages.add(smsData);
}
}
if (toSaveMessages.size() > 0) {
this.smsMessageRepository.saveAll(toSaveMessages);
this.smsMessageRepository.flush();
this.genericExecutorService.execute(new SmsTask(apiQueueResourceDatas, ThreadLocalContextUtil.getContext()));
}
if (!toSendNotificationMessages.isEmpty()) {
this.notificationSenderService.sendNotification(toSendNotificationMessages);
}
// new MyThread(ThreadLocalContextUtil.getTenant(),
// apiQueueResourceDatas).start();
}
} catch (Exception e) {
throw new ConnectionFailureException(SmsCampaignConstants.SMS, e);
}
page++;
totalRecords = pendingMessages.getTotalPages();
} while (page < totalRecords);
}
private void connectAndSendToIntermediateServer(Collection<SmsMessageApiQueueResourceData> apiQueueResourceDatas) {
Map<String, Object> hostConfig = this.smsConfigUtils.getMessageGateWayRequestURI("sms",
SmsMessageApiQueueResourceData.toJsonString(apiQueueResourceDatas));
@@ -172,7 +97,6 @@ public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJob
for (Map.Entry<SmsCampaign, Collection<SmsMessage>> entry : smsDataMap.entrySet()) {
Iterator<SmsMessage> smsMessageIterator = entry.getValue().iterator();
Collection<SmsMessageApiQueueResourceData> apiQueueResourceDatas = new ArrayList<>();
StringBuilder request = new StringBuilder();
while (smsMessageIterator.hasNext()) {
SmsMessage smsMessage = smsMessageIterator.next();
if (smsMessage.isNotification()) {
@@ -223,95 +147,6 @@ public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJob
}
}
/**
* get SMS message delivery reports from the SMS gateway (or intermediate gateway)
**/
@Override
@Transactional
@CronTarget(jobName = JobName.GET_DELIVERY_REPORTS_FROM_SMS_GATEWAY)
public void getDeliveryReports() {
int page = 0;
int totalRecords = 0;
Integer limit = 200;
do {
Page<Long> smsMessageInternalIds = this.smsReadPlatformService.retrieveAllWaitingForDeliveryReport(limit);
// only proceed if there are sms message with status type enum 300
try {
if (smsMessageInternalIds.getPageItems().size() > 0) {
// make request
Map<String, Object> hostConfig = this.smsConfigUtils.getMessageGateWayRequestURI("sms/report",
new Gson().toJson(smsMessageInternalIds.getPageItems()));
URI uri = (URI) hostConfig.get("uri");
HttpEntity<?> entity = (HttpEntity<?>) hostConfig.get("entity");
ResponseEntity<Collection<SmsMessageDeliveryReportData>> responseOne = restTemplate.exchange(uri, HttpMethod.POST,
entity, new ParameterizedTypeReference<Collection<SmsMessageDeliveryReportData>>() {
});
Collection<SmsMessageDeliveryReportData> smsMessageDeliveryReportDatas = responseOne.getBody();
Iterator<SmsMessageDeliveryReportData> responseReportIterator = smsMessageDeliveryReportDatas.iterator();
while (responseReportIterator.hasNext()) {
SmsMessageDeliveryReportData smsMessageDeliveryReportData = responseReportIterator.next();
Integer deliveryStatus = smsMessageDeliveryReportData.getDeliveryStatus();
if (!smsMessageDeliveryReportData.getHasError() && deliveryStatus != 100) {
SmsMessage smsMessage = this.smsMessageRepository.findById(smsMessageDeliveryReportData.getId()).orElse(null);
Integer statusType = smsMessage.getStatusType();
switch (deliveryStatus) {
case 0:
statusType = SmsMessageStatusType.INVALID.getValue();
break;
case 150:
statusType = SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue();
break;
case 200:
statusType = SmsMessageStatusType.SENT.getValue();
break;
case 300:
statusType = SmsMessageStatusType.DELIVERED.getValue();
break;
case 400:
statusType = SmsMessageStatusType.FAILED.getValue();
break;
default:
statusType = smsMessage.getStatusType();
break;
}
boolean statusChanged = !statusType.equals(smsMessage.getStatusType());
// update the status Type enum
smsMessage.setStatusType(statusType);
// update the externalId
smsMessage.setExternalId(smsMessageDeliveryReportData.getExternalId());
// save the SmsMessage entity
this.smsMessageRepository.save(smsMessage);
if (statusChanged) {
log.info("Status of SMS message id: {} successfully changed to {}", smsMessage.getId(), statusType);
}
}
}
if (smsMessageDeliveryReportDatas.size() > 0) {
log.info("{} delivery report(s) successfully received from the intermediate gateway - sms",
smsMessageDeliveryReportDatas.size());
}
}
} catch (Exception e) {
log.error("Error occured.", e);
}
page++;
totalRecords = smsMessageInternalIds.getTotalFilteredRecords();
} while (page < totalRecords);
}
class SmsTask implements Runnable, ApplicationListener<ContextClosedEvent> {
private final FineractContext context;
@@ -168,9 +168,7 @@ public class SmsReadPlatformServiceImpl implements SmsReadPlatformService {
final String sqlPlusLimit = limit > 0 ? " " + sqlGenerator.limit(limit) : "";
final String sql = "select id from " + this.smsRowMapper.tableName() + " where status_enum = "
+ SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue() + sqlPlusLimit;
return this.paginationHelper.fetchPage(jdbcTemplate, sql, Long.class);
// (this.jdbcTemplate, sqlCountRows, new Object [] {}, Long.class);
// this.jdbcTemplate.queryForList(sql, Long.class);
return paginationHelper.fetchPage(jdbcTemplate, sql, Long.class);
}
@Override
@@ -0,0 +1,67 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.account.jobs.executestandinginstructions;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class ExecuteStandingInstructionsConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private StandingInstructionReadPlatformService standingInstructionReadPlatformService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private DatabaseSpecificSQLGenerator sqlGenerator;
@Autowired
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Bean
protected Step executeStandingInstructionsStep() {
return steps.get(JobName.EXECUTE_STANDING_INSTRUCTIONS.name()).tasklet(executeStandingInstructionsTasklet()).build();
}
@Bean
public Job executeStandingInstructionsJob() {
return jobs.get(JobName.EXECUTE_STANDING_INSTRUCTIONS.name()).start(executeStandingInstructionsStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() {
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
accountTransfersWritePlatformService);
}
}
@@ -0,0 +1,166 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.account.jobs.executestandinginstructions;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException;
import org.springframework.batch.core.StepContribution;
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.jdbc.core.JdbcTemplate;
@Slf4j
@RequiredArgsConstructor
public class ExecuteStandingInstructionsTasklet implements Tasklet {
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
Collection<StandingInstructionData> instructionData = standingInstructionReadPlatformService
.retrieveAll(StandingInstructionStatus.ACTIVE.getValue());
List<Throwable> errors = new ArrayList<>();
for (StandingInstructionData data : instructionData) {
boolean isDueForTransfer = false;
AccountTransferRecurrenceType recurrenceType = data.recurrenceType();
StandingInstructionType instructionType = data.instructionType();
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
if (recurrenceType.isPeriodicRecurrence()) {
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
PeriodFrequencyType frequencyType = data.recurrenceFrequency();
LocalDate startDate = data.validFrom();
if (frequencyType.isMonthly()) {
startDate = startDate.withDayOfMonth(data.recurrenceOnDay());
if (startDate.isBefore(data.validFrom())) {
startDate = startDate.plusMonths(1);
}
} else if (frequencyType.isYearly()) {
startDate = startDate.withDayOfMonth(data.recurrenceOnDay()).withMonth(data.recurrenceOnMonth());
if (startDate.isBefore(data.validFrom())) {
startDate = startDate.plusYears(1);
}
}
isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.recurrenceInterval(), startDate,
transactionDate);
}
BigDecimal transactionAmount = data.amount();
if (data.toAccountType().isLoanAccount()
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
StandingInstructionDuesData standingInstructionDuesData = standingInstructionReadPlatformService
.retriveLoanDuesData(data.toAccount().accountId());
if (data.instructionType().isDuesAmoutTransfer()) {
transactionAmount = standingInstructionDuesData.totalDueAmount();
}
if (recurrenceType.isDuesRecurrence()) {
isDueForTransfer = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).equals(standingInstructionDuesData.dueDate());
}
}
if (isDueForTransfer && transactionAmount != null && transactionAmount.compareTo(BigDecimal.ZERO) > 0) {
final SavingsAccount fromSavingsAccount = null;
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false;
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount, data.fromAccountType(),
data.toAccountType(), data.fromAccount().accountId(), data.toAccount().accountId(),
data.name() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null,
data.transferType().getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction,
isExceptionForBalanceCheck);
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
if (transferCompleted) {
final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?";
jdbcTemplate.update(updateQuery, transactionDate, data.getId());
}
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
return RepeatStatus.FINISHED;
}
private boolean transferAmount(final List<Throwable> errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) {
boolean transferCompleted = true;
StringBuilder errorLog = new StringBuilder();
StringBuilder updateQuery = new StringBuilder(
"INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status")
+ ", amount,execution_time, error_log) VALUES (");
try {
accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (final PlatformApiDataValidationException e) {
errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Validation exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (final InsufficientAccountBalanceException e) {
errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id"
+ instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("InsufficientAccountBalance Exception ");
} catch (final AbstractPlatformServiceUnavailableException e) {
errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Platform exception while trasfering funds ").append(e.getDefaultUserMessage());
} catch (Exception e) {
errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId
+ " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Exception while trasfering funds ").append(e.getMessage());
}
updateQuery.append(instructionId).append(",");
if (errorLog.length() > 0) {
transferCompleted = false;
updateQuery.append("'failed'").append(",");
} else {
updateQuery.append("'success'").append(",");
}
updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue());
updateQuery.append(", now(),");
updateQuery.append("'").append(errorLog).append("')");
jdbcTemplate.update(updateQuery.toString());
return transferCompleted;
}
}
@@ -20,7 +20,6 @@ package org.apache.fineract.portfolio.account.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
public interface StandingInstructionWritePlatformService {
@@ -28,8 +27,6 @@ public interface StandingInstructionWritePlatformService {
CommandProcessingResult update(Long id, JsonCommand command);
void executeStandingInstructions() throws JobExecutionException;
CommandProcessingResult delete(Long id);
}
@@ -23,85 +23,39 @@ import static org.apache.fineract.portfolio.account.AccountDetailConstants.fromC
import static org.apache.fineract.portfolio.account.AccountDetailConstants.toAccountTypeParamName;
import static org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants.statusParamName;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.api.StandingInstructionApiConstants;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDataValidator;
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
import org.apache.fineract.portfolio.account.domain.StandingInstructionAssembler;
import org.apache.fineract.portfolio.account.domain.StandingInstructionRepository;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
import org.apache.fineract.portfolio.account.exception.StandingInstructionNotFoundException;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.exception.InsufficientAccountBalanceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class StandingInstructionWritePlatformServiceImpl implements StandingInstructionWritePlatformService {
private static final Logger LOG = LoggerFactory.getLogger(StandingInstructionWritePlatformServiceImpl.class);
private final StandingInstructionDataValidator standingInstructionDataValidator;
private final StandingInstructionAssembler standingInstructionAssembler;
private final AccountTransferDetailRepository accountTransferDetailRepository;
private final StandingInstructionRepository standingInstructionRepository;
private final StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
@Autowired
public StandingInstructionWritePlatformServiceImpl(final StandingInstructionDataValidator standingInstructionDataValidator,
final StandingInstructionAssembler standingInstructionAssembler,
final AccountTransferDetailRepository accountTransferDetailRepository,
final StandingInstructionRepository standingInstructionRepository,
final StandingInstructionReadPlatformService standingInstructionReadPlatformService,
final AccountTransfersWritePlatformService accountTransfersWritePlatformService, final JdbcTemplate jdbcTemplate,
DatabaseSpecificSQLGenerator sqlGenerator) {
this.standingInstructionDataValidator = standingInstructionDataValidator;
this.standingInstructionAssembler = standingInstructionAssembler;
this.accountTransferDetailRepository = accountTransferDetailRepository;
this.standingInstructionRepository = standingInstructionRepository;
this.standingInstructionReadPlatformService = standingInstructionReadPlatformService;
this.accountTransfersWritePlatformService = accountTransfersWritePlatformService;
this.jdbcTemplate = jdbcTemplate;
this.sqlGenerator = sqlGenerator;
}
@Transactional
@Override
@@ -152,7 +106,7 @@ public class StandingInstructionWritePlatformServiceImpl implements StandingInst
throw new PlatformDataIntegrityException("error.msg.standinginstruction.duplicate.name",
"Standinginstruction with name `" + name + "` already exists", "name", name);
}
LOG.error("Error occured.", dve);
log.error("Error occured.", dve);
throw new PlatformDataIntegrityException("error.msg.client.unknown.data.integrity.issue",
"Unknown data integrity issue with resource.");
}
@@ -190,110 +144,4 @@ public class StandingInstructionWritePlatformServiceImpl implements StandingInst
actualChanges.put(statusParamName, StandingInstructionStatus.DELETED.getValue());
return new CommandProcessingResultBuilder().withEntityId(id).with(actualChanges).build();
}
@Override
@CronTarget(jobName = JobName.EXECUTE_STANDING_INSTRUCTIONS)
public void executeStandingInstructions() throws JobExecutionException {
Collection<StandingInstructionData> instructionDatas = this.standingInstructionReadPlatformService
.retrieveAll(StandingInstructionStatus.ACTIVE.getValue());
List<Throwable> errors = new ArrayList<>();
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
for (StandingInstructionData data : instructionDatas) {
boolean isDueForTransfer = false;
AccountTransferRecurrenceType recurrenceType = data.recurrenceType();
StandingInstructionType instructionType = data.instructionType();
if (recurrenceType.isPeriodicRecurrence()) {
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
PeriodFrequencyType frequencyType = data.recurrenceFrequency();
LocalDate startDate = data.validFrom();
if (frequencyType.isMonthly()) {
startDate = startDate.withDayOfMonth(data.recurrenceOnDay());
if (startDate.isBefore(data.validFrom())) {
startDate = startDate.plusMonths(1);
}
} else if (frequencyType.isYearly()) {
startDate = startDate.withDayOfMonth(data.recurrenceOnDay()).withMonth(data.recurrenceOnMonth());
if (startDate.isBefore(data.validFrom())) {
startDate = startDate.plusYears(1);
}
}
isDueForTransfer = scheduledDateGenerator.isDateFallsInSchedule(frequencyType, data.recurrenceInterval(), startDate,
transactionDate);
}
BigDecimal transactionAmount = data.amount();
if (data.toAccountType().isLoanAccount()
&& (recurrenceType.isDuesRecurrence() || (isDueForTransfer && instructionType.isDuesAmoutTransfer()))) {
StandingInstructionDuesData standingInstructionDuesData = this.standingInstructionReadPlatformService
.retriveLoanDuesData(data.toAccount().accountId());
if (data.instructionType().isDuesAmoutTransfer()) {
transactionAmount = standingInstructionDuesData.totalDueAmount();
}
if (recurrenceType.isDuesRecurrence()) {
isDueForTransfer = DateUtils.getBusinessLocalDate().equals(standingInstructionDuesData.dueDate());
}
}
if (isDueForTransfer && transactionAmount != null && transactionAmount.compareTo(BigDecimal.ZERO) > 0) {
final SavingsAccount fromSavingsAccount = null;
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false;
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount, data.fromAccountType(),
data.toAccountType(), data.fromAccount().accountId(), data.toAccount().accountId(),
data.name() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null,
data.transferType().getValue(), null, null, null, null, null, fromSavingsAccount, isRegularTransaction,
isExceptionForBalanceCheck);
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
if (transferCompleted) {
final String updateQuery = "UPDATE m_account_transfer_standing_instructions SET last_run_date = ? where id = ?";
this.jdbcTemplate.update(updateQuery, transactionDate, data.getId());
}
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
}
private boolean transferAmount(final List<Throwable> errors, final AccountTransferDTO accountTransferDTO, final Long instructionId) {
boolean transferCompleted = true;
StringBuilder errorLog = new StringBuilder();
StringBuilder updateQuery = new StringBuilder(
"INSERT INTO m_account_transfer_standing_instructions_history (standing_instruction_id, " + sqlGenerator.escape("status")
+ ", amount, execution_time, error_log) VALUES (");
try {
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (final PlatformApiDataValidationException e) {
errors.add(new Exception("Validation exception while transfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Validation exception while trasfering funds " + e.getDefaultUserMessage());
} catch (final InsufficientAccountBalanceException e) {
errors.add(new Exception("InsufficientAccountBalance Exception while trasfering funds for standing Instruction id"
+ instructionId + " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("InsufficientAccountBalance Exception ");
} catch (final AbstractPlatformServiceUnavailableException e) {
errors.add(new Exception("Platform exception while trasfering funds for standing Instruction id" + instructionId + " from "
+ accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Platform exception while trasfering funds " + e.getDefaultUserMessage());
} catch (Exception e) {
errors.add(new Exception("Unhandled System Exception while trasfering funds for standing Instruction id" + instructionId
+ " from " + accountTransferDTO.getFromAccountId() + " to " + accountTransferDTO.getToAccountId(), e));
errorLog.append("Exception while trasfering funds " + e.getMessage());
}
updateQuery.append(instructionId).append(",");
if (errorLog.length() > 0) {
transferCompleted = false;
updateQuery.append("'failed'").append(",");
} else {
updateQuery.append("'success'").append(",");
}
updateQuery.append(accountTransferDTO.getTransactionAmount().doubleValue());
updateQuery.append(", ").append(sqlGenerator.currentTenantDateTime()).append(" ");
updateQuery.append(", '").append(errorLog).append("')");
this.jdbcTemplate.update(updateQuery.toString());
return transferCompleted;
}
}
@@ -0,0 +1,50 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.addaccrualentries;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AddAccrualEntriesConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Bean
protected Step addAccrualEntriesStep(AddAccrualEntriesTasklet addAccrualEntriesTasklet) {
return steps.get(JobName.ADD_ACCRUAL_ENTRIES.name()).tasklet(addAccrualEntriesTasklet).build();
}
@Bean
public Job addAccrualEntriesJob(AddAccrualEntriesTasklet addAccrualEntriesTasklet) {
return jobs.get(JobName.ADD_ACCRUAL_ENTRIES.name()).start(addAccrualEntriesStep(addAccrualEntriesTasklet))
.incrementer(new RunIdIncrementer()).build();
}
}
@@ -0,0 +1,74 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.addaccrualentries;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.batch.core.StepContribution;
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.stereotype.Component;
@Slf4j
@RequiredArgsConstructor
@Component
public class AddAccrualEntriesTasklet implements Tasklet {
private final LoanReadPlatformService loanReadPlatformService;
private final LoanAccrualWritePlatformService loanAccrualWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas = loanReadPlatformService.retriveScheduleAccrualData();
Map<Long, Collection<LoanScheduleAccrualData>> loanDataMap = new HashMap<>();
for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDatas) {
if (loanDataMap.containsKey(accrualData.getLoanId())) {
loanDataMap.get(accrualData.getLoanId()).add(accrualData);
} else {
Collection<LoanScheduleAccrualData> accrualDatas = new ArrayList<>();
accrualDatas.add(accrualData);
loanDataMap.put(accrualData.getLoanId(), accrualDatas);
}
}
List<Throwable> errors = new ArrayList<>();
for (Map.Entry<Long, Collection<LoanScheduleAccrualData>> mapEntry : loanDataMap.entrySet()) {
try {
loanAccrualWritePlatformService.addAccrualAccounting(mapEntry.getKey(), mapEntry.getValue());
} catch (Exception e) {
log.error("Failed to add accural transaction for loan {}", mapEntry.getKey(), e);
errors.add(e);
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,57 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentries;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AddPeriodicAccrualEntriesConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private LoanAccrualPlatformService loanAccrualPlatformService;
@Bean
protected Step addPeriodicAccrualEntriesStep() {
return steps.get(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES.name()).tasklet(addPeriodicAccrualEntriesTasklet()).build();
}
@Bean
public Job addPeriodicAccrualEntriesJob() {
return jobs.get(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES.name()).start(addPeriodicAccrualEntriesStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public AddPeriodicAccrualEntriesTasklet addPeriodicAccrualEntriesTasklet() {
return new AddPeriodicAccrualEntriesTasklet(loanAccrualPlatformService);
}
}
@@ -0,0 +1,52 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentries;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class AddPeriodicAccrualEntriesTasklet implements Tasklet {
private final LoanAccrualPlatformService loanAccrualPlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
try {
addPeriodicAccruals(DateUtils.getBusinessLocalDate());
} catch (MultiException e) {
throw new JobExecutionException(e);
}
return RepeatStatus.FINISHED;
}
private void addPeriodicAccruals(final LocalDate tilldate) throws MultiException {
loanAccrualPlatformService.addPeriodicAccruals(tilldate);
}
}
@@ -0,0 +1,62 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentriesforloanswithincomepostedastransactions;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AddPeriodicAccrualEntriesForLoansConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private LoanReadPlatformService loanReadPlatformService;
@Autowired
private LoanAccrualWritePlatformService loanAccrualWritePlatformService;
@Bean
protected Step addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsStep() {
return steps.get(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS.name())
.tasklet(addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsTasklet()).build();
}
@Bean
public Job addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsJob() {
return jobs.get(JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS.name())
.start(addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsStep()).incrementer(new RunIdIncrementer()).build();
}
@Bean
public AddPeriodicAccrualEntriesForLoansTasklet addPeriodicAccrualEntriesForLoansWithIncomePostedAsTransactionsTasklet() {
return new AddPeriodicAccrualEntriesForLoansTasklet(loanReadPlatformService, loanAccrualWritePlatformService);
}
}
@@ -0,0 +1,61 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.addperiodicaccrualentriesforloanswithincomepostedastransactions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class AddPeriodicAccrualEntriesForLoansTasklet implements Tasklet {
private final LoanReadPlatformService loanReadPlatformService;
private final LoanAccrualWritePlatformService loanAccrualWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
Collection<Long> loanIds = loanReadPlatformService.retrieveLoanIdsWithPendingIncomePostingTransactions();
if (!CollectionUtils.isEmpty(loanIds)) {
List<Throwable> errors = new ArrayList<>();
for (Long loanId : loanIds) {
try {
loanAccrualWritePlatformService.addIncomeAndAccrualTransactions(loanId);
} catch (Exception e) {
log.error("Failed to add income and accrual transaction for loan {}", loanId, e);
errors.add(e);
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
}
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,66 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.applychargetooverdueloaninstallment;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ApplyChargeToOverdueLoanInstallmentConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private ConfigurationDomainService configurationDomainService;
@Autowired
private LoanReadPlatformService loanReadPlatformService;
@Autowired
private LoanWritePlatformService loanWritePlatformService;
@Bean
protected Step applyChargeToOverdueLoanInstallmentStep() {
return steps.get(JobName.APPLY_CHARGE_TO_OVERDUE_LOAN_INSTALLMENT.name()).tasklet(applyChargeToOverdueLoanInstallmentTasklet())
.build();
}
@Bean
public Job applyChargeToOverdueLoanInstallmentsJob() {
return jobs.get(JobName.APPLY_CHARGE_TO_OVERDUE_LOAN_INSTALLMENT.name()).start(applyChargeToOverdueLoanInstallmentStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public ApplyChargeToOverdueLoanInstallmentTasklet applyChargeToOverdueLoanInstallmentTasklet() {
return new ApplyChargeToOverdueLoanInstallmentTasklet(configurationDomainService, loanReadPlatformService,
loanWritePlatformService);
}
}
@@ -0,0 +1,95 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.applychargetooverdueloaninstallment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class ApplyChargeToOverdueLoanInstallmentTasklet implements Tasklet {
private final ConfigurationDomainService configurationDomainService;
private final LoanReadPlatformService loanReadPlatformService;
private final LoanWritePlatformService loanWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final Long penaltyWaitPeriodValue = configurationDomainService.retrievePenaltyWaitPeriod();
final Boolean backdatePenalties = configurationDomainService.isBackdatePenaltiesEnabled();
final Collection<OverdueLoanScheduleData> overdueLoanScheduledInstallments = loanReadPlatformService
.retrieveAllLoansWithOverdueInstallments(penaltyWaitPeriodValue, backdatePenalties);
if (!overdueLoanScheduledInstallments.isEmpty()) {
final Map<Long, Collection<OverdueLoanScheduleData>> overdueScheduleData = new HashMap<>();
for (final OverdueLoanScheduleData overdueInstallment : overdueLoanScheduledInstallments) {
if (overdueScheduleData.containsKey(overdueInstallment.getLoanId())) {
overdueScheduleData.get(overdueInstallment.getLoanId()).add(overdueInstallment);
} else {
Collection<OverdueLoanScheduleData> loanData = new ArrayList<>();
loanData.add(overdueInstallment);
overdueScheduleData.put(overdueInstallment.getLoanId(), loanData);
}
}
List<Throwable> exceptions = new ArrayList<>();
for (final Long loanId : overdueScheduleData.keySet()) {
try {
loanWritePlatformService.applyOverdueChargesForLoan(loanId, overdueScheduleData.get(loanId));
} catch (final PlatformApiDataValidationException e) {
final List<ApiParameterError> errors = e.getErrors();
for (final ApiParameterError error : errors) {
log.error("Apply Charges due for overdue loans failed for account {} with message: {}", loanId,
error.getDeveloperMessage(), e);
}
exceptions.add(e);
} catch (final AbstractPlatformDomainRuleException e) {
log.error("Apply Charges due for overdue loans failed for account {} with message: {}", loanId,
e.getDefaultUserMessage(), e);
exceptions.add(e);
} catch (Exception e) {
log.error("Apply Charges due for overdue loans failed for account {}", loanId, e);
exceptions.add(e);
}
}
if (!exceptions.isEmpty()) {
throw new JobExecutionException(exceptions);
}
}
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,67 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.applyholidaystoloans;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ApplyHolidaysToLoansConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private ConfigurationDomainService configurationDomainService;
@Autowired
private HolidayRepositoryWrapper holidayRepository;
@Autowired
private LoanRepositoryWrapper loanRepositoryWrapper;
@Autowired
private LoanUtilService loanUtilService;
@Bean
protected Step applyHolidaysToLoansStep() {
return steps.get(JobName.APPLY_HOLIDAYS_TO_LOANS.name()).tasklet(applyHolidaysToLoansTasklet()).build();
}
@Bean
public Job applyHolidaysToLoansJob() {
return jobs.get(JobName.APPLY_HOLIDAYS_TO_LOANS.name()).start(applyHolidaysToLoansStep()).incrementer(new RunIdIncrementer())
.build();
}
@Bean
public ApplyHolidaysToLoansTasklet applyHolidaysToLoansTasklet() {
return new ApplyHolidaysToLoansTasklet(configurationDomainService, holidayRepository, loanRepositoryWrapper, loanUtilService);
}
}
@@ -0,0 +1,83 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.applyholidaystoloans;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class ApplyHolidaysToLoansTasklet implements Tasklet {
private final ConfigurationDomainService configurationDomainService;
private final HolidayRepositoryWrapper holidayRepository;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanUtilService loanUtilService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final boolean isHolidayEnabled = configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
if (!isHolidayEnabled) {
return RepeatStatus.FINISHED;
}
final Collection<Integer> loanStatuses = new ArrayList<>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(),
LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue()));
final List<Holiday> holidays = holidayRepository.findUnprocessed();
for (final Holiday holiday : holidays) {
final Set<Office> offices = holiday.getOffices();
final Collection<Long> officeIds = new ArrayList<>(offices.size());
for (final Office office : offices) {
officeIds.add(office.getId());
}
final List<Loan> loans = new ArrayList<>();
loans.addAll(loanRepositoryWrapper.findByClientOfficeIdsAndLoanStatus(officeIds, loanStatuses));
// FIXME: AA optimize to get all client and group loans belongs to an office id
loans.addAll(loanRepositoryWrapper.findByGroupOfficeIdsAndLoanStatus(officeIds, loanStatuses));
for (final Loan loan : loans) {
loan.applyHolidayToRepaymentScheduleDates(holiday, loanUtilService);
}
loanRepositoryWrapper.save(loans);
holiday.processed();
}
holidayRepository.save(holidays);
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,60 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.generateloanlossprovisioning;
import org.apache.fineract.accounting.provisioning.service.ProvisioningEntriesWritePlatformService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.organisation.provisioning.service.ProvisioningCriteriaReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GenerateLoanlossProvisioningConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private ProvisioningCriteriaReadPlatformService provisioningCriteriaReadPlatformService;
@Autowired
private ProvisioningEntriesWritePlatformService provisioningEntriesWritePlatformService;
@Bean
protected Step generateLoanlossProvisioningStep() {
return steps.get(JobName.GENERATE_LOANLOSS_PROVISIONING.name()).tasklet(generateLoanlossProvisioningTasklet()).build();
}
@Bean
public Job generateLoanlossProvisioningJob() {
return jobs.get(JobName.GENERATE_LOANLOSS_PROVISIONING.name()).start(generateLoanlossProvisioningStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public GenerateLoanlossProvisioningTasklet generateLoanlossProvisioningTasklet() {
return new GenerateLoanlossProvisioningTasklet(provisioningCriteriaReadPlatformService, provisioningEntriesWritePlatformService);
}
}
@@ -0,0 +1,62 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.generateloanlossprovisioning;
import java.time.LocalDate;
import java.util.Collection;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.provisioning.exception.ProvisioningEntryAlreadyCreatedException;
import org.apache.fineract.accounting.provisioning.service.ProvisioningEntriesWritePlatformService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.provisioning.data.ProvisioningCriteriaData;
import org.apache.fineract.organisation.provisioning.service.ProvisioningCriteriaReadPlatformService;
import org.springframework.batch.core.StepContribution;
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.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
@Slf4j
@RequiredArgsConstructor
public class GenerateLoanlossProvisioningTasklet implements Tasklet {
private final ProvisioningCriteriaReadPlatformService provisioningCriteriaReadPlatformService;
private final ProvisioningEntriesWritePlatformService provisioningEntriesWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
LocalDate currentDate = DateUtils.getBusinessLocalDate();
boolean addJournalEntries = true;
try {
Collection<ProvisioningCriteriaData> criteriaCollection = provisioningCriteriaReadPlatformService
.retrieveAllProvisioningCriterias();
if (criteriaCollection == null || criteriaCollection.size() == 0) {
throw new NoProvisioningCriteriaDefinitionFoundException();
}
provisioningEntriesWritePlatformService.createProvisioningEntry(currentDate, addJournalEntries);
} catch (ProvisioningEntryAlreadyCreatedException peace) {
log.error("Provisioning Entry already created", peace);
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
log.error("Problem occurred in generateLoanLossProvisioningAmount function", dve);
}
return RepeatStatus.FINISHED;
}
}
@@ -16,9 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.adhocquery.service;
package org.apache.fineract.portfolio.loanaccount.jobs.generateloanlossprovisioning;
public interface AdHocScheduledJobRunnerService {
void generateClientSchedule();
}
public class NoProvisioningCriteriaDefinitionFoundException extends RuntimeException {}
@@ -0,0 +1,72 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.recalculateinterestforloan;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.RecalculateInterestPoster;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RecalculateInterestForLoanConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private LoanReadPlatformService loanReadPlatformService;
@Autowired
private LoanWritePlatformService loanWritePlatformService;
@Autowired
private RecalculateInterestPoster recalculateInterestPoster;
@Autowired
private OfficeReadPlatformService officeReadPlatformService;
@Bean
protected Step recalculateInterestForLoanStep() {
return steps.get(JobName.RECALCULATE_INTEREST_FOR_LOAN.name()).tasklet(recalculateInterestForLoanTasklet()).build();
}
@Bean
public Job recalculateInterestForLoanJob() {
return jobs.get(JobName.RECALCULATE_INTEREST_FOR_LOAN.name()).start(recalculateInterestForLoanStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public RecalculateInterestForLoanTasklet recalculateInterestForLoanTasklet() {
return new RecalculateInterestForLoanTasklet(loanReadPlatformService, loanWritePlatformService, recalculateInterestPoster,
officeReadPlatformService);
}
}
@@ -0,0 +1,228 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.recalculateinterestforloan;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.organisation.office.data.OfficeData;
import org.apache.fineract.organisation.office.exception.OfficeNotFoundException;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.RecalculateInterestPoster;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.StepContribution;
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.dao.CannotAcquireLockException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
@Slf4j
@RequiredArgsConstructor
public class RecalculateInterestForLoanTasklet implements Tasklet {
private final LoanReadPlatformService loanReadPlatformService;
private final LoanWritePlatformService loanWritePlatformService;
private final RecalculateInterestPoster recalculateInterestPoster;
private final OfficeReadPlatformService officeReadPlatformService;
private static final SecureRandom random = new SecureRandom();
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
Map<String, JobParameter> jobParameters = chunkContext.getStepContext().getStepExecution().getJobParameters().getParameters();
if (!jobParameters.isEmpty()) {
final String officeId = (String) jobParameters.get("officeId").getValue();
log.info("recalculateInterest: officeId={}", officeId);
Long officeIdLong = Long.valueOf(officeId);
final OfficeData office = officeReadPlatformService.retrieveOffice(officeIdLong);
if (office == null) {
throw new OfficeNotFoundException(officeIdLong);
}
final int threadPoolSize = Integer.parseInt((String) jobParameters.get("thread-pool-size").getValue());
final int batchSize = Integer.parseInt((String) jobParameters.get("batch-size").getValue());
recalculateInterest(office, threadPoolSize, batchSize);
} else {
int maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock();
int maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries();
Collection<Long> loanIds = loanReadPlatformService.fetchLoansForInterestRecalculation();
int i = 0;
if (!loanIds.isEmpty()) {
List<Throwable> errors = new ArrayList<>();
for (Long loanId : loanIds) {
log.info("recalculateInterest: Loan ID = {}", loanId);
int numberOfRetries = 0;
while (numberOfRetries <= maxNumberOfRetries) {
try {
loanWritePlatformService.recalculateInterest(loanId);
numberOfRetries = maxNumberOfRetries + 1;
} catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) {
log.info("Recalulate interest job has been retried {} time(s)", numberOfRetries);
if (numberOfRetries >= maxNumberOfRetries) {
log.error(
"Recalulate interest job has been retried for the max allowed attempts of {} and will be rolled back",
numberOfRetries);
errors.add(exception);
break;
}
try {
int randomNum = random.nextInt(maxIntervalBetweenRetries + 1);
Thread.sleep(1000 + (randomNum * 1000L));
numberOfRetries = numberOfRetries + 1;
} catch (InterruptedException e) {
log.error("Interest recalculation for loans retry failed due to InterruptedException", e);
errors.add(e);
break;
}
} catch (Exception e) {
log.error("Interest recalculation for loans failed for account {}", loanId, e);
numberOfRetries = maxNumberOfRetries + 1;
errors.add(e);
}
i++;
}
log.info("recalculateInterest: Loans count {}", i);
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
}
}
return RepeatStatus.FINISHED;
}
private void recalculateInterest(OfficeData office, int threadPoolSize, int batchSize) {
final int pageSize = batchSize * threadPoolSize;
final ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize);
Long maxLoanIdInList = 0L;
final String officeHierarchy = office.getHierarchy() + "%";
List<Long> loanIds = Collections.synchronizedList(
this.loanReadPlatformService.fetchLoansForInterestRecalculation(pageSize, maxLoanIdInList, officeHierarchy));
do {
int totalFilteredRecords = loanIds.size();
log.info("Starting accrual - total filtered records - {}", totalFilteredRecords);
recalculateInterest(loanIds, threadPoolSize, batchSize, executorService);
maxLoanIdInList += pageSize + 1;
loanIds = Collections.synchronizedList(
this.loanReadPlatformService.fetchLoansForInterestRecalculation(pageSize, maxLoanIdInList, officeHierarchy));
} while (!CollectionUtils.isEmpty(loanIds));
executorService.shutdownNow();
}
private void recalculateInterest(List<Long> loanIds, int threadPoolSize, int batchSize, final ExecutorService executorService) {
List<Callable<Void>> posters = new ArrayList<>();
int fromIndex = 0;
int size = loanIds.size();
double toGetCeilValue = size / threadPoolSize;
batchSize = (int) Math.ceil(toGetCeilValue);
if (batchSize == 0) {
return;
}
int toIndex = (batchSize > size - 1) ? size : batchSize;
while (toIndex < size && loanIds.get(toIndex - 1).equals(loanIds.get(toIndex))) {
toIndex++;
}
boolean lastBatch = false;
int loopCount = size / batchSize + 1;
for (long i = 0; i < loopCount; i++) {
List<Long> subList = safeSubList(loanIds, fromIndex, toIndex);
recalculateInterestPoster.setLoanIds(subList);
recalculateInterestPoster.setLoanWritePlatformService(loanWritePlatformService);
posters.add(recalculateInterestPoster);
if (lastBatch) {
break;
}
if (toIndex + batchSize > size - 1) {
lastBatch = true;
}
fromIndex = fromIndex + (toIndex - fromIndex);
toIndex = (toIndex + batchSize > size - 1) ? size : toIndex + batchSize;
while (toIndex < size && loanIds.get(toIndex - 1).equals(loanIds.get(toIndex))) {
toIndex++;
}
}
try {
List<Future<Void>> responses = executorService.invokeAll(posters);
checkCompletion(responses);
} catch (InterruptedException e1) {
log.error("Interrupted while recalculateInterest", e1);
}
}
private <T> List<T> safeSubList(List<T> list, int fromIndex, int toIndex) {
int size = list.size();
if (fromIndex >= size || toIndex <= 0 || fromIndex >= toIndex) {
return Collections.emptyList();
}
fromIndex = Math.max(0, fromIndex);
toIndex = Math.min(size, toIndex);
return list.subList(fromIndex, toIndex);
}
private void checkCompletion(List<Future<Void>> responses) {
try {
for (Future<Void> f : responses) {
f.get();
}
boolean allThreadsExecuted = false;
int noOfThreadsExecuted = 0;
for (Future<Void> future : responses) {
if (future.isDone()) {
noOfThreadsExecuted++;
}
}
allThreadsExecuted = noOfThreadsExecuted == responses.size();
if (!allThreadsExecuted) {
log.error("All threads could not execute.");
}
} catch (InterruptedException e1) {
log.error("Interrupted while posting IR entries", e1);
} catch (ExecutionException e2) {
log.error("Execution exception while posting IR entries", e2);
}
}
}
@@ -0,0 +1,65 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.transferfeechargeforloans;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TransferFeeChargeForLoansConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private LoanChargeReadPlatformService loanChargeReadPlatformService;
@Autowired
private AccountAssociationsReadPlatformService accountAssociationsReadPlatformService;
@Autowired
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Bean
protected Step transferFeeChargeForLoansStep() {
return steps.get(JobName.TRANSFER_FEE_CHARGE_FOR_LOANS.name()).tasklet(transferFeeChargeForLoansTasklet()).build();
}
@Bean
public Job transferFeeChargeForLoansJob() {
return jobs.get(JobName.TRANSFER_FEE_CHARGE_FOR_LOANS.name()).start(transferFeeChargeForLoansStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public TransferFeeChargeForLoansTasklet transferFeeChargeForLoansTasklet() {
return new TransferFeeChargeForLoansTasklet(loanChargeReadPlatformService, accountAssociationsReadPlatformService,
accountTransfersWritePlatformService);
}
}
@@ -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.portfolio.loanaccount.jobs.transferfeechargeforloans;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.account.PortfolioAccountType;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.PortfolioAccountData;
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class TransferFeeChargeForLoansTasklet implements Tasklet {
private final LoanChargeReadPlatformService loanChargeReadPlatformService;
private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final Collection<LoanChargeData> chargeDatas = loanChargeReadPlatformService
.retrieveLoanChargesForFeePayment(ChargePaymentMode.ACCOUNT_TRANSFER.getValue(), LoanStatus.ACTIVE.getValue());
final boolean isRegularTransaction = true;
List<Throwable> errors = new ArrayList<>();
if (chargeDatas != null) {
for (final LoanChargeData chargeData : chargeDatas) {
if (chargeData.isInstallmentFee()) {
final Collection<LoanInstallmentChargeData> chargePerInstallments = loanChargeReadPlatformService
.retrieveInstallmentLoanCharges(chargeData.getId(), true);
PortfolioAccountData portfolioAccountData = null;
for (final LoanInstallmentChargeData installmentChargeData : chargePerInstallments) {
if (!installmentChargeData.getDueDate().isAfter(DateUtils.getBusinessLocalDate())) {
if (portfolioAccountData == null) {
portfolioAccountData = accountAssociationsReadPlatformService
.retriveLoanLinkedAssociation(chargeData.getLoanId());
}
final boolean isExceptionForBalanceCheck = false;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(DateUtils.getBusinessLocalDate(),
installmentChargeData.getAmountOutstanding(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN,
portfolioAccountData.accountId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null, null,
LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(),
installmentChargeData.getInstallmentNumber(), AccountTransferType.CHARGE_PAYMENT.getValue(), null, null,
null, null, null, null, isRegularTransaction, isExceptionForBalanceCheck);
transferFeeCharge(accountTransferDTO, errors);
}
}
} else if (chargeData.getDueDate() != null && !chargeData.getDueDate().isAfter(DateUtils.getBusinessLocalDate())) {
final PortfolioAccountData portfolioAccountData = accountAssociationsReadPlatformService
.retriveLoanLinkedAssociation(chargeData.getLoanId());
final boolean isExceptionForBalanceCheck = false;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(DateUtils.getBusinessLocalDate(),
chargeData.getAmountOutstanding(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN,
portfolioAccountData.accountId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null, null,
LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(), null,
AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, null, isRegularTransaction,
isExceptionForBalanceCheck);
transferFeeCharge(accountTransferDTO, errors);
}
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
return RepeatStatus.FINISHED;
}
private void transferFeeCharge(final AccountTransferDTO accountTransferDTO, List<Throwable> errors) {
try {
accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (RuntimeException e) {
log.error("Exception while paying charge {} for loan id {}", accountTransferDTO.getChargeId(),
accountTransferDTO.getToAccountId(), e);
errors.add(e);
}
}
}
@@ -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.portfolio.loanaccount.jobs.updateloanarrearsageing;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class UpdateLoanArrearsAgeingConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private DatabaseSpecificSQLGenerator sqlGenerator;
@Autowired
private LoanArrearsAgingService loanArrearsAgingService;
@Bean
protected Step updateLoanArrearsAgeingStep() {
return steps.get(JobName.UPDATE_LOAN_ARREARS_AGEING.name()).tasklet(updateLoanArrearsAgeingTasklet()).build();
}
@Bean
public Job updateLoanArrearsAgeingJob() {
return jobs.get(JobName.UPDATE_LOAN_ARREARS_AGEING.name()).start(updateLoanArrearsAgeingStep()).incrementer(new RunIdIncrementer())
.build();
}
@Bean
public UpdateLoanArrearsAgeingTasklet updateLoanArrearsAgeingTasklet() {
return new UpdateLoanArrearsAgeingTasklet(jdbcTemplate, sqlGenerator, loanArrearsAgingService);
}
}
@@ -0,0 +1,130 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.updateloanarrearsageing;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService;
import org.springframework.batch.core.StepContribution;
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.jdbc.core.JdbcTemplate;
@Slf4j
@RequiredArgsConstructor
public class UpdateLoanArrearsAgeingTasklet implements Tasklet {
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final LoanArrearsAgingService loanArrearsAgingService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
jdbcTemplate.execute("truncate table m_loan_arrears_aging");
final StringBuilder updateSqlBuilder = new StringBuilder(900);
final String principalOverdueCalculationSql = "SUM(COALESCE(mr.principal_amount, 0) - coalesce(mr.principal_completed_derived, 0) - coalesce(mr.principal_writtenoff_derived, 0))";
final String interestOverdueCalculationSql = "SUM(COALESCE(mr.interest_amount, 0) - coalesce(mr.interest_writtenoff_derived, 0) - coalesce(mr.interest_waived_derived, 0) - "
+ "coalesce(mr.interest_completed_derived, 0))";
final String feeChargesOverdueCalculationSql = "SUM(COALESCE(mr.fee_charges_amount, 0) - coalesce(mr.fee_charges_writtenoff_derived, 0) - "
+ "coalesce(mr.fee_charges_waived_derived, 0) - coalesce(mr.fee_charges_completed_derived, 0))";
final String penaltyChargesOverdueCalculationSql = "SUM(COALESCE(mr.penalty_charges_amount, 0) - coalesce(mr.penalty_charges_writtenoff_derived, 0) - "
+ "coalesce(mr.penalty_charges_waived_derived, 0) - coalesce(mr.penalty_charges_completed_derived, 0))";
updateSqlBuilder.append(
"INSERT INTO m_loan_arrears_aging(loan_id,principal_overdue_derived,interest_overdue_derived,fee_charges_overdue_derived,penalty_charges_overdue_derived,total_overdue_derived,overdue_since_date_derived)");
updateSqlBuilder.append("select ml.id as loanId,");
updateSqlBuilder.append(principalOverdueCalculationSql + " as principal_overdue_derived,");
updateSqlBuilder.append(interestOverdueCalculationSql + " as interest_overdue_derived,");
updateSqlBuilder.append(feeChargesOverdueCalculationSql + " as fee_charges_overdue_derived,");
updateSqlBuilder.append(penaltyChargesOverdueCalculationSql + " as penalty_charges_overdue_derived,");
updateSqlBuilder.append(principalOverdueCalculationSql + "+" + interestOverdueCalculationSql + "+");
updateSqlBuilder.append(feeChargesOverdueCalculationSql + "+" + penaltyChargesOverdueCalculationSql + " as total_overdue_derived,");
updateSqlBuilder.append("MIN(mr.duedate) as overdue_since_date_derived ");
updateSqlBuilder.append(" FROM m_loan ml ");
updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
updateSqlBuilder.append(" left join m_product_loan_recalculation_details prd on prd.product_id = ml.product_id ");
updateSqlBuilder.append(" WHERE ml.loan_status_id = 300 "); // active
updateSqlBuilder.append(" and mr.completed_derived is false ");
updateSqlBuilder.append(" and mr.duedate < ")
.append(sqlGenerator.subDate(sqlGenerator.currentBusinessDate(), "COALESCE(ml.grace_on_arrears_ageing, 0)", "day"))
.append(" ");
updateSqlBuilder.append(" and (prd.arrears_based_on_original_schedule = false or prd.arrears_based_on_original_schedule is null) ");
updateSqlBuilder.append(" GROUP BY ml.id");
List<String> insertStatements = updateLoanArrearsAgeingDetailsWithOriginalSchedule();
insertStatements.add(0, updateSqlBuilder.toString());
final int[] results = this.jdbcTemplate.batchUpdate(insertStatements.toArray(new String[0]));
int result = 0;
for (int i : results) {
result += i;
}
log.info("{}: Records affected by updateLoanArrearsAgeingDetails: {}", ThreadLocalContextUtil.getTenant().getName(), result);
return RepeatStatus.FINISHED;
}
private List<String> updateLoanArrearsAgeingDetailsWithOriginalSchedule() {
List<String> insertStatement = new ArrayList<>();
final StringBuilder loanIdentifier = new StringBuilder();
loanIdentifier.append("select ml.id as loanId FROM m_loan ml ");
loanIdentifier.append("INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
loanIdentifier.append(
"inner join m_product_loan_recalculation_details prd on prd.product_id = ml.product_id and prd.arrears_based_on_original_schedule = true ");
loanIdentifier.append("WHERE ml.loan_status_id = 300 and mr.completed_derived is false and mr.duedate < ")
.append(sqlGenerator.subDate(sqlGenerator.currentBusinessDate(), "COALESCE(ml.grace_on_arrears_ageing, 0)", "day"))
.append(" group by ml.id");
List<Long> loanIds = this.jdbcTemplate.queryForList(loanIdentifier.toString(), Long.class);
if (!loanIds.isEmpty()) {
String loanIdsAsString = loanIds.toString();
loanIdsAsString = loanIdsAsString.substring(1, loanIdsAsString.length() - 1);
Map<Long, List<LoanSchedulePeriodData>> scheduleDate = loanArrearsAgingService.getScheduleDate(loanIdsAsString);
List<Map<String, Object>> loanSummary = getLoanSummary(loanIdsAsString);
loanArrearsAgingService.updateScheduleWithPaidDetail(scheduleDate, loanSummary);
loanArrearsAgingService.createInsertStatements(insertStatement, scheduleDate, true);
}
return insertStatement;
}
private List<Map<String, Object>> getLoanSummary(final String loanIdsAsString) {
final StringBuilder transactionsSql = new StringBuilder();
transactionsSql.append("select ml.id as loanId, ");
transactionsSql
.append("ml.principal_repaid_derived as principalAmtPaid, ml.principal_writtenoff_derived as principalAmtWrittenoff, ");
transactionsSql.append(" ml.interest_repaid_derived as interestAmtPaid, ml.interest_waived_derived as interestAmtWaived, ");
transactionsSql.append("ml.fee_charges_repaid_derived as feeAmtPaid, ml.fee_charges_waived_derived as feeAmtWaived, ");
transactionsSql
.append("ml.penalty_charges_repaid_derived as penaltyAmtPaid, ml.penalty_charges_waived_derived as penaltyAmtWaived ");
transactionsSql.append("from m_loan ml ");
transactionsSql.append("where ml.id IN (").append(loanIdsAsString).append(") order by ml.id");
List<Map<String, Object>> loanSummary = this.jdbcTemplate.queryForList(transactionsSql.toString());
return loanSummary;
}
}
@@ -21,7 +21,6 @@ package org.apache.fineract.portfolio.loanaccount.service;
import java.time.LocalDate;
import java.util.Collection;
import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
public interface LoanAccrualPlatformService {
@@ -30,10 +29,4 @@ public interface LoanAccrualPlatformService {
void addPeriodicAccruals(LocalDate tilldate, Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas) throws MultiException;
void addAccrualAccounting() throws JobExecutionException;
void addPeriodicAccruals() throws JobExecutionException;
void addPeriodicAccrualsForLoansWithIncomePostedAsTransactions() throws JobExecutionException;
}
@@ -24,71 +24,20 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class LoanAccrualPlatformServiceImpl implements LoanAccrualPlatformService {
private static final Logger LOG = LoggerFactory.getLogger(LoanAccrualPlatformServiceImpl.class);
private final LoanReadPlatformService loanReadPlatformService;
private final LoanAccrualWritePlatformService loanAccrualWritePlatformService;
@Autowired
public LoanAccrualPlatformServiceImpl(final LoanReadPlatformService loanReadPlatformService,
final LoanAccrualWritePlatformService loanAccrualWritePlatformService) {
this.loanReadPlatformService = loanReadPlatformService;
this.loanAccrualWritePlatformService = loanAccrualWritePlatformService;
}
@Override
@CronTarget(jobName = JobName.ADD_ACCRUAL_ENTRIES)
public void addAccrualAccounting() throws JobExecutionException {
Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas = this.loanReadPlatformService.retriveScheduleAccrualData();
Map<Long, Collection<LoanScheduleAccrualData>> loanDataMap = new HashMap<>();
for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDatas) {
if (loanDataMap.containsKey(accrualData.getLoanId())) {
loanDataMap.get(accrualData.getLoanId()).add(accrualData);
} else {
Collection<LoanScheduleAccrualData> accrualDatas = new ArrayList<>();
accrualDatas.add(accrualData);
loanDataMap.put(accrualData.getLoanId(), accrualDatas);
}
}
List<Throwable> errors = new ArrayList<>();
for (Map.Entry<Long, Collection<LoanScheduleAccrualData>> mapEntry : loanDataMap.entrySet()) {
try {
this.loanAccrualWritePlatformService.addAccrualAccounting(mapEntry.getKey(), mapEntry.getValue());
} catch (Exception e) {
LOG.error("Failed to add accural transaction for loan {}", mapEntry.getKey(), e);
errors.add(e);
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
}
@Override
@CronTarget(jobName = JobName.ADD_PERIODIC_ACCRUAL_ENTRIES)
public void addPeriodicAccruals() throws JobExecutionException {
try {
addPeriodicAccruals(DateUtils.getBusinessLocalDate());
} catch (MultiException e) {
throw new JobExecutionException(e);
}
}
@Override
public void addPeriodicAccruals(final LocalDate tilldate) throws JobExecutionException {
Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas = this.loanReadPlatformService.retrivePeriodicAccrualData(tilldate);
@@ -114,7 +63,7 @@ public class LoanAccrualPlatformServiceImpl implements LoanAccrualPlatformServic
try {
this.loanAccrualWritePlatformService.addPeriodicAccruals(tilldate, mapEntry.getKey(), mapEntry.getValue());
} catch (Exception e) {
LOG.error("Failed to add accural transaction for loan {}", mapEntry.getKey(), e);
log.error("Failed to add accural transaction for loan {}", mapEntry.getKey(), e);
errors.add(e);
}
}
@@ -122,24 +71,4 @@ public class LoanAccrualPlatformServiceImpl implements LoanAccrualPlatformServic
throw new JobExecutionException(errors);
}
}
@Override
@CronTarget(jobName = JobName.ADD_PERIODIC_ACCRUAL_ENTRIES_FOR_LOANS_WITH_INCOME_POSTED_AS_TRANSACTIONS)
public void addPeriodicAccrualsForLoansWithIncomePostedAsTransactions() throws JobExecutionException {
Collection<Long> loanIds = this.loanReadPlatformService.retrieveLoanIdsWithPendingIncomePostingTransactions();
if (loanIds != null && loanIds.size() > 0) {
List<Throwable> errors = new ArrayList<>();
for (Long loanId : loanIds) {
try {
this.loanAccrualWritePlatformService.addIncomeAndAccrualTransactions(loanId);
} catch (Exception e) {
LOG.error("Failed to add income and accrual transaction for loan {}", loanId, e);
errors.add(e);
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
}
}
}
@@ -28,6 +28,7 @@ import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
@@ -47,13 +48,13 @@ import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlatformService {
private final LoanReadPlatformService loanReadPlatformService;
@@ -65,22 +66,6 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository;
@Autowired
public LoanAccrualWritePlatformServiceImpl(final JdbcTemplate jdbcTemplate, final LoanReadPlatformService loanReadPlatformService,
final JournalEntryWritePlatformService journalEntryWritePlatformService,
final LoanChargeReadPlatformService loanChargeReadPlatformService, final AppUserRepositoryWrapper userRepository,
final LoanRepositoryWrapper loanRepositoryWrapper, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository,
DatabaseSpecificSQLGenerator sqlGenerator) {
this.loanReadPlatformService = loanReadPlatformService;
this.sqlGenerator = sqlGenerator;
this.jdbcTemplate = jdbcTemplate;
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
this.loanChargeReadPlatformService = loanChargeReadPlatformService;
this.userRepository = userRepository;
this.loanRepositoryWrapper = loanRepositoryWrapper;
this.applicationCurrencyRepository = applicationCurrencyRepository;
}
@Override
@Transactional
public void addAccrualAccounting(final Long loanId, final Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas)
@@ -503,10 +488,10 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
private void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
final MonetaryCurrency currency = loan.getCurrency();
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
final ApplicationCurrency applicationCurrency = applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(applicationCurrency.toData(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
}
@@ -18,14 +18,21 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;
import java.util.List;
import java.util.Map;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
public interface LoanArrearsAgingService {
void updateLoanArrearsAgeingDetails();
void updateLoanArrearsAgeingDetailsWithOriginalSchedule(Loan loan);
Map<Long, List<LoanSchedulePeriodData>> getScheduleDate(String loanId);
void updateLoanArrearsAgeingDetails(Loan loan);
void createInsertStatements(List<String> insertStatement, Map<Long, List<LoanSchedulePeriodData>> scheduleDate,
boolean isInsertStatement);
void updateScheduleWithPaidDetail(Map<Long, List<LoanSchedulePeriodData>> scheduleDate, List<Map<String, Object>> loanSummary);
}
@@ -32,10 +32,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.businessevent.BusinessEventListener;
import org.apache.fineract.portfolio.businessevent.domain.loan.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.portfolio.businessevent.domain.loan.LoanApplyOverdueChargeBusinessEvent;
@@ -59,7 +56,6 @@ import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
@@ -92,54 +88,6 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
new LoanForeClosureEventListener());
}
@Transactional
@Override
@CronTarget(jobName = JobName.UPDATE_LOAN_ARREARS_AGEING)
public void updateLoanArrearsAgeingDetails() {
this.jdbcTemplate.execute("truncate table m_loan_arrears_aging");
final StringBuilder updateSqlBuilder = new StringBuilder(900);
final String principalOverdueCalculationSql = "SUM(COALESCE(mr.principal_amount, 0) - coalesce(mr.principal_completed_derived, 0) - coalesce(mr.principal_writtenoff_derived, 0))";
final String interestOverdueCalculationSql = "SUM(COALESCE(mr.interest_amount, 0) - coalesce(mr.interest_writtenoff_derived, 0) - coalesce(mr.interest_waived_derived, 0) - "
+ "coalesce(mr.interest_completed_derived, 0))";
final String feeChargesOverdueCalculationSql = "SUM(COALESCE(mr.fee_charges_amount, 0) - coalesce(mr.fee_charges_writtenoff_derived, 0) - "
+ "coalesce(mr.fee_charges_waived_derived, 0) - coalesce(mr.fee_charges_completed_derived, 0))";
final String penaltyChargesOverdueCalculationSql = "SUM(COALESCE(mr.penalty_charges_amount, 0) - coalesce(mr.penalty_charges_writtenoff_derived, 0) - "
+ "coalesce(mr.penalty_charges_waived_derived, 0) - coalesce(mr.penalty_charges_completed_derived, 0))";
updateSqlBuilder.append(
"INSERT INTO m_loan_arrears_aging(loan_id,principal_overdue_derived,interest_overdue_derived,fee_charges_overdue_derived,penalty_charges_overdue_derived,total_overdue_derived,overdue_since_date_derived)");
updateSqlBuilder.append("select ml.id as loanId,");
updateSqlBuilder.append(principalOverdueCalculationSql + " as principal_overdue_derived,");
updateSqlBuilder.append(interestOverdueCalculationSql + " as interest_overdue_derived,");
updateSqlBuilder.append(feeChargesOverdueCalculationSql + " as fee_charges_overdue_derived,");
updateSqlBuilder.append(penaltyChargesOverdueCalculationSql + " as penalty_charges_overdue_derived,");
updateSqlBuilder.append(principalOverdueCalculationSql + "+" + interestOverdueCalculationSql + "+");
updateSqlBuilder.append(feeChargesOverdueCalculationSql + "+" + penaltyChargesOverdueCalculationSql + " as total_overdue_derived,");
updateSqlBuilder.append("MIN(mr.duedate) as overdue_since_date_derived ");
updateSqlBuilder.append(" FROM m_loan ml ");
updateSqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
updateSqlBuilder.append(" left join m_product_loan_recalculation_details prd on prd.product_id = ml.product_id ");
updateSqlBuilder.append(" WHERE ml.loan_status_id = 300 "); // active
updateSqlBuilder.append(" and mr.completed_derived is false ");
updateSqlBuilder.append(" and mr.duedate < ")
.append(sqlGenerator.subDate(sqlGenerator.currentBusinessDate(), "COALESCE(ml.grace_on_arrears_ageing, 0)", "day"))
.append(" ");
updateSqlBuilder.append(" and (prd.arrears_based_on_original_schedule = false or prd.arrears_based_on_original_schedule is null) ");
updateSqlBuilder.append(" GROUP BY ml.id");
List<String> insertStatements = updateLoanArrearsAgeingDetailsWithOriginalSchedule();
insertStatements.add(0, updateSqlBuilder.toString());
final int[] results = this.jdbcTemplate.batchUpdate(insertStatements.toArray(new String[0]));
int result = 0;
for (int i : results) {
result += i;
}
log.info("{}: Records affected by updateLoanArrearsAgeingDetails: {}", ThreadLocalContextUtil.getTenant().getName(), result);
}
@Override
public void updateLoanArrearsAgeingDetailsWithOriginalSchedule(final Loan loan) {
int count = this.jdbcTemplate.queryForObject("select count(mla.loan_id) from m_loan_arrears_aging mla where mla.loan_id =?",
@@ -150,7 +98,7 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
originalScheduleExtractor);
if (scheduleDate.size() > 0) {
List<Map<String, Object>> transactions = getLoanSummary(loan.getId(), loan.getLoanSummary());
updateSchheduleWithPaidDetail(scheduleDate, transactions);
updateScheduleWithPaidDetail(scheduleDate, transactions);
createInsertStatements(updateStatement, scheduleDate, count == 0);
if (updateStatement.size() == 1) {
this.jdbcTemplate.update(updateStatement.get(0));
@@ -161,6 +109,12 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
}
}
@Override
public Map<Long, List<LoanSchedulePeriodData>> getScheduleDate(String loanId) {
OriginalScheduleExtractor originalScheduleExtractor = new OriginalScheduleExtractor(loanId, sqlGenerator);
return this.jdbcTemplate.query(originalScheduleExtractor.schema, originalScheduleExtractor);
}
@Override
public void updateLoanArrearsAgeingDetails(final Loan loan) {
int count = this.jdbcTemplate.queryForObject("select count(mla.loan_id) from m_loan_arrears_aging mla where mla.loan_id =?",
@@ -208,50 +162,6 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
return updateSql;
}
private List<String> updateLoanArrearsAgeingDetailsWithOriginalSchedule() {
List<String> insertStatement = new ArrayList<>();
final StringBuilder loanIdentifier = new StringBuilder();
loanIdentifier.append("select ml.id as loanId FROM m_loan ml ");
loanIdentifier.append("INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
loanIdentifier.append(
"inner join m_product_loan_recalculation_details prd on prd.product_id = ml.product_id and prd.arrears_based_on_original_schedule = true ");
loanIdentifier.append("WHERE ml.loan_status_id = 300 and mr.completed_derived is false and mr.duedate < ")
.append(sqlGenerator.subDate(sqlGenerator.currentBusinessDate(), "COALESCE(ml.grace_on_arrears_ageing, 0)", "day"))
.append(" group by ml.id");
List<Long> loanIds = this.jdbcTemplate.queryForList(loanIdentifier.toString(), Long.class);
if (!loanIds.isEmpty()) {
String loanIdsAsString = loanIds.toString();
loanIdsAsString = loanIdsAsString.substring(1, loanIdsAsString.length() - 1);
OriginalScheduleExtractor originalScheduleExtractor = new OriginalScheduleExtractor(loanIdsAsString, sqlGenerator);
Map<Long, List<LoanSchedulePeriodData>> scheduleDate = this.jdbcTemplate.query(originalScheduleExtractor.schema,
originalScheduleExtractor);
List<Map<String, Object>> loanSummary = getLoanSummary(loanIdsAsString);
updateSchheduleWithPaidDetail(scheduleDate, loanSummary);
createInsertStatements(insertStatement, scheduleDate, true);
}
return insertStatement;
}
private List<Map<String, Object>> getLoanSummary(final String loanIdsAsString) {
final StringBuilder transactionsSql = new StringBuilder();
transactionsSql.append("select ml.id as loanId, ");
transactionsSql
.append("ml.principal_repaid_derived as principalAmtPaid, ml.principal_writtenoff_derived as principalAmtWrittenoff, ");
transactionsSql.append(" ml.interest_repaid_derived as interestAmtPaid, ml.interest_waived_derived as interestAmtWaived, ");
transactionsSql.append("ml.fee_charges_repaid_derived as feeAmtPaid, ml.fee_charges_waived_derived as feeAmtWaived, ");
transactionsSql
.append("ml.penalty_charges_repaid_derived as penaltyAmtPaid, ml.penalty_charges_waived_derived as penaltyAmtWaived ");
transactionsSql.append("from m_loan ml ");
transactionsSql.append("where ml.id IN (").append(loanIdsAsString).append(") order by ml.id");
List<Map<String, Object>> loanSummary = this.jdbcTemplate.queryForList(transactionsSql.toString());
return loanSummary;
}
private List<Map<String, Object>> getLoanSummary(final Long loanId, final LoanSummary loanSummary) {
List<Map<String, Object>> transactionDetail = new ArrayList<>();
Map<String, Object> transactionMap = new HashMap<>();
@@ -270,7 +180,8 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
}
private void createInsertStatements(List<String> insertStatement, Map<Long, List<LoanSchedulePeriodData>> scheduleDate,
@Override
public void createInsertStatements(List<String> insertStatement, Map<Long, List<LoanSchedulePeriodData>> scheduleDate,
boolean isInsertStatement) {
for (Map.Entry<Long, List<LoanSchedulePeriodData>> entry : scheduleDate.entrySet()) {
final Long loanId = entry.getKey();
@@ -341,25 +252,10 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
return insertStatementBuilder.toString();
}
private void updateSchheduleWithPaidDetail(Map<Long, List<LoanSchedulePeriodData>> scheduleDate,
List<Map<String, Object>> loanSummary) {
@Override
public void updateScheduleWithPaidDetail(Map<Long, List<LoanSchedulePeriodData>> scheduleDate, List<Map<String, Object>> loanSummary) {
for (Map<String, Object> transactionMap : loanSummary) {
String longValue = transactionMap.get("loanId").toString(); // From
// JDBC
// Template
// API,
// we
// are
// getting
// BigInteger
// but
// in
// other
// call,
// we
// are
// getting
// Long
String longValue = transactionMap.get("loanId").toString();
Long loanId = Long.parseLong(longValue);
BigDecimal principalAmtPaid = (BigDecimal) transactionMap.get("principalAmtPaid");
BigDecimal principalAmtWrittenoff = (BigDecimal) transactionMap.get("principalAmtWrittenoff");
@@ -375,15 +271,15 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
BigDecimal feeAmt = feeAmtPaid.add(feeAmtWaived);
BigDecimal penaltyAmt = penaltyAmtPaid.add(penaltyAmtWaived);
List<LoanSchedulePeriodData> loanSchedulePeriodDatas = scheduleDate.get(loanId);
if (loanSchedulePeriodDatas != null) {
List<LoanSchedulePeriodData> updatedPeriodData = new ArrayList<>(loanSchedulePeriodDatas.size());
for (LoanSchedulePeriodData loanSchedulePeriodData : loanSchedulePeriodDatas) {
BigDecimal principalPaid = null;
BigDecimal interestPaid = null;
BigDecimal feeChargesPaid = null;
BigDecimal penaltyChargesPaid = null;
Boolean isComplete = true;
List<LoanSchedulePeriodData> loanSchedulePeriodDataList = scheduleDate.get(loanId);
if (loanSchedulePeriodDataList != null) {
List<LoanSchedulePeriodData> updatedPeriodData = new ArrayList<>(loanSchedulePeriodDataList.size());
for (LoanSchedulePeriodData loanSchedulePeriodData : loanSchedulePeriodDataList) {
BigDecimal principalPaid;
BigDecimal interestPaid;
BigDecimal feeChargesPaid;
BigDecimal penaltyChargesPaid;
boolean isComplete = true;
if (loanSchedulePeriodData.principalDue().compareTo(principalAmt) > 0) {
principalPaid = principalAmt;
principalAmt = BigDecimal.ZERO;
@@ -422,8 +318,8 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
principalPaid, interestPaid, feeChargesPaid, penaltyChargesPaid);
updatedPeriodData.add(periodData);
}
loanSchedulePeriodDatas.clear();
loanSchedulePeriodDatas.addAll(updatedPeriodData);
loanSchedulePeriodDataList.clear();
loanSchedulePeriodDataList.addAll(updatedPeriodData);
}
}
}
@@ -1,296 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.service;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.loan.ApplyChargeToOverdueLoansBusinessStep;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.organisation.office.data.OfficeData;
import org.apache.fineract.organisation.office.exception.OfficeNotFoundException;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
import org.springframework.context.ApplicationContext;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
@Service
@RequiredArgsConstructor
@Slf4j
public class LoanSchedularServiceImpl implements LoanSchedularService {
private static final SecureRandom RANDOM = new SecureRandom();
private final ConfigurationDomainService configurationDomainService;
private final LoanReadPlatformService loanReadPlatformService;
private final LoanWritePlatformService loanWritePlatformService;
private final OfficeReadPlatformService officeReadPlatformService;
private final ApplicationContext applicationContext;
private final ApplyChargeToOverdueLoansBusinessStep applyChargeToOverdueLoansBusinessStep;
private final LoanRepository loanRepository;
@Override
@CronTarget(jobName = JobName.APPLY_CHARGE_TO_OVERDUE_LOAN_INSTALLMENT)
public void applyChargeForOverdueLoans() throws JobExecutionException {
final Long penaltyWaitPeriodValue = this.configurationDomainService.retrievePenaltyWaitPeriod();
final Boolean backdatePenalties = this.configurationDomainService.isBackdatePenaltiesEnabled();
final Collection<OverdueLoanScheduleData> overdueLoanScheduledInstallments = this.loanReadPlatformService
.retrieveAllLoansWithOverdueInstallments(penaltyWaitPeriodValue, backdatePenalties);
Set<Long> loanIds = overdueLoanScheduledInstallments.stream().map(OverdueLoanScheduleData::getLoanId).collect(Collectors.toSet());
if (!loanIds.isEmpty()) {
List<Throwable> exceptions = new ArrayList<>();
for (final Long loanId : loanIds) {
try {
applyChargeToOverdueLoansBusinessStep.execute(loanRepository.getReferenceById(loanId));
} catch (final PlatformApiDataValidationException e) {
final List<ApiParameterError> errors = e.getErrors();
for (final ApiParameterError error : errors) {
log.error("Apply Charges due for overdue loans failed for account {} with message: {}", loanId,
error.getDeveloperMessage(), e);
}
exceptions.add(e);
} catch (final AbstractPlatformDomainRuleException e) {
log.error("Apply Charges due for overdue loans failed for account {} with message: {}", loanId,
e.getDefaultUserMessage(), e);
exceptions.add(e);
} catch (Exception e) {
log.error("Apply Charges due for overdue loans failed for account {}", loanId, e);
exceptions.add(e);
}
}
if (!exceptions.isEmpty()) {
throw new JobExecutionException(exceptions);
}
}
}
@Override
@CronTarget(jobName = JobName.RECALCULATE_INTEREST_FOR_LOAN)
@SuppressFBWarnings(value = {
"DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once")
public void recalculateInterest() throws JobExecutionException {
Integer maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock();
Integer maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries();
Collection<Long> loanIds = this.loanReadPlatformService.fetchLoansForInterestRecalculation();
int i = 0;
if (!loanIds.isEmpty()) {
List<Throwable> errors = new ArrayList<>();
for (Long loanId : loanIds) {
log.info("recalculateInterest: Loan ID = {}", loanId);
Integer numberOfRetries = 0;
while (numberOfRetries <= maxNumberOfRetries) {
try {
this.loanWritePlatformService.recalculateInterest(loanId);
numberOfRetries = maxNumberOfRetries + 1;
} catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) {
log.info("Recalulate interest job has been retried {} time(s)", numberOfRetries);
// Fail if the transaction has been retried for
// maxNumberOfRetries
if (numberOfRetries >= maxNumberOfRetries) {
log.error("Recalulate interest job has been retried for the max allowed attempts of {} and will be rolled back",
numberOfRetries);
errors.add(exception);
break;
}
// Else sleep for a random time (between 1 to 10
// seconds) and continue
try {
int randomNum = RANDOM.nextInt(maxIntervalBetweenRetries + 1);
Thread.sleep(1000 + (randomNum * 1000));
numberOfRetries = numberOfRetries + 1;
} catch (InterruptedException e) {
log.error("Interest recalculation for loans retry failed due to InterruptedException", e);
errors.add(e);
break;
}
} catch (Exception e) {
log.error("Interest recalculation for loans failed for account {}", loanId, e);
numberOfRetries = maxNumberOfRetries + 1;
errors.add(e);
}
i++;
}
log.info("recalculateInterest: Loans count {}", i);
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
}
}
@Override
@CronTarget(jobName = JobName.RECALCULATE_INTEREST_FOR_LOAN)
public void recalculateInterest(Map<String, String> jobParameters) {
// gets the officeId
final String officeId = jobParameters.get("officeId");
log.info("recalculateInterest: officeId={}", officeId);
Long officeIdLong = Long.valueOf(officeId);
// gets the Office object
final OfficeData office = this.officeReadPlatformService.retrieveOffice(officeIdLong);
if (office == null) {
throw new OfficeNotFoundException(officeIdLong);
}
final int threadPoolSize = Integer.parseInt(jobParameters.get("thread-pool-size"));
final int batchSize = Integer.parseInt(jobParameters.get("batch-size"));
recalculateInterest(office, threadPoolSize, batchSize);
}
private void recalculateInterest(OfficeData office, int threadPoolSize, int batchSize) {
final int pageSize = batchSize * threadPoolSize;
// initialise the executor service with fetched configurations
final ExecutorService executorService = Executors.newFixedThreadPool(threadPoolSize);
Long maxLoanIdInList = 0L;
final String officeHierarchy = office.getHierarchy() + "%";
// get the loanIds from service
List<Long> loanIds = Collections.synchronizedList(
this.loanReadPlatformService.fetchLoansForInterestRecalculation(pageSize, maxLoanIdInList, officeHierarchy));
// gets the loanIds data set iteratively and call addAccuruals for that
// paginated dataset
do {
int totalFilteredRecords = loanIds.size();
log.info("Starting accrual - total filtered records - {}", totalFilteredRecords);
recalculateInterest(loanIds, threadPoolSize, batchSize, executorService);
maxLoanIdInList += pageSize + 1;
loanIds = Collections.synchronizedList(
this.loanReadPlatformService.fetchLoansForInterestRecalculation(pageSize, maxLoanIdInList, officeHierarchy));
} while (!CollectionUtils.isEmpty(loanIds));
// shutdown the executor when done
executorService.shutdownNow();
}
private void recalculateInterest(List<Long> loanIds, int threadPoolSize, int batchSize, final ExecutorService executorService) {
List<Callable<Void>> posters = new ArrayList<>();
int fromIndex = 0;
// get the size of current paginated dataset
int size = loanIds.size();
// calculate the batch size
double toGetCeilValue = size / threadPoolSize;
batchSize = (int) Math.ceil(toGetCeilValue);
if (batchSize == 0) {
return;
}
int toIndex = (batchSize > size - 1) ? size : batchSize;
while (toIndex < size && loanIds.get(toIndex - 1).equals(loanIds.get(toIndex))) {
toIndex++;
}
boolean lastBatch = false;
int loopCount = size / batchSize + 1;
for (long i = 0; i < loopCount; i++) {
List<Long> subList = safeSubList(loanIds, fromIndex, toIndex);
RecalculateInterestPoster poster = (RecalculateInterestPoster) this.applicationContext.getBean("recalculateInterestPoster");
poster.setLoanIds(subList);
poster.setLoanWritePlatformService(loanWritePlatformService);
posters.add(poster);
if (lastBatch) {
break;
}
if (toIndex + batchSize > size - 1) {
lastBatch = true;
}
fromIndex = fromIndex + (toIndex - fromIndex);
toIndex = (toIndex + batchSize > size - 1) ? size : toIndex + batchSize;
while (toIndex < size && loanIds.get(toIndex - 1).equals(loanIds.get(toIndex))) {
toIndex++;
}
}
try {
List<Future<Void>> responses = executorService.invokeAll(posters);
checkCompletion(responses);
} catch (InterruptedException e1) {
log.error("Interrupted while recalculateInterest", e1);
}
}
// break the lists into sub lists
private <T> List<T> safeSubList(List<T> list, int fromIndex, int toIndex) {
int size = list.size();
if (fromIndex >= size || toIndex <= 0 || fromIndex >= toIndex) {
return Collections.emptyList();
}
fromIndex = Math.max(0, fromIndex);
toIndex = Math.min(size, toIndex);
return list.subList(fromIndex, toIndex);
}
// checks the execution of task by each thread in the executor service
private void checkCompletion(List<Future<Void>> responses) {
try {
for (Future<Void> f : responses) {
f.get();
}
boolean allThreadsExecuted = false;
int noOfThreadsExecuted = 0;
for (Future<Void> future : responses) {
if (future.isDone()) {
noOfThreadsExecuted++;
}
}
allThreadsExecuted = noOfThreadsExecuted == responses.size();
if (!allThreadsExecuted) {
log.error("All threads could not execute.");
}
} catch (InterruptedException e1) {
log.error("Interrupted while posting IR entries", e1);
} catch (ExecutionException e2) {
log.error("Execution exception while posting IR entries", e2);
}
}
}
@@ -23,7 +23,6 @@ import java.util.Collection;
import java.util.Map;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
@@ -78,8 +77,6 @@ public interface LoanWritePlatformService {
void applyMeetingDateChanges(Calendar calendar, Collection<CalendarInstance> loanCalendarInstances,
Boolean reschedulebasedOnMeetingDates, LocalDate presentMeetingDate, LocalDate newMeetingDate);
void applyHolidaysToLoans();
LoanTransaction initiateLoanTransfer(Loan loan, LocalDate transferDate);
LoanTransaction withdrawLoanTransfer(Loan loan, LocalDate transferDate);
@@ -90,8 +87,6 @@ public interface LoanWritePlatformService {
CommandProcessingResult payLoanCharge(Long loanId, Long loanChargeId, JsonCommand command, boolean isChargeIdIncludedInJson);
void transferFeeCharges() throws JobExecutionException;
CommandProcessingResult undoWriteOff(Long loanId);
CommandProcessingResult updateDisbursementDateAndAmountForTranche(Long loanId, Long disbursementId, JsonCommand command);
@@ -54,9 +54,6 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum;
import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService;
import org.apache.fineract.infrastructure.jobs.annotation.CronTarget;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
@@ -121,7 +118,6 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarRepository;
import org.apache.fineract.portfolio.calendar.domain.CalendarType;
import org.apache.fineract.portfolio.calendar.exception.CalendarParameterUpdateNotSupportedException;
import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeUpdatedException;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException;
@@ -150,9 +146,7 @@ import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.command.LoanUpdateCommand;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData;
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine;
@@ -2032,66 +2026,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
@Override
@CronTarget(jobName = JobName.TRANSFER_FEE_CHARGE_FOR_LOANS)
public void transferFeeCharges() throws JobExecutionException {
final Collection<LoanChargeData> chargeDatas = this.loanChargeReadPlatformService
.retrieveLoanChargesForFeePayment(ChargePaymentMode.ACCOUNT_TRANSFER.getValue(), LoanStatus.ACTIVE.getValue());
final boolean isRegularTransaction = true;
List<Throwable> errors = new ArrayList<>();
if (chargeDatas != null) {
for (final LoanChargeData chargeData : chargeDatas) {
if (chargeData.isInstallmentFee()) {
final Collection<LoanInstallmentChargeData> chargePerInstallments = this.loanChargeReadPlatformService
.retrieveInstallmentLoanCharges(chargeData.getId(), true);
PortfolioAccountData portfolioAccountData = null;
for (final LoanInstallmentChargeData installmentChargeData : chargePerInstallments) {
if (!installmentChargeData.getDueDate().isAfter(DateUtils.getBusinessLocalDate())) {
if (portfolioAccountData == null) {
portfolioAccountData = this.accountAssociationsReadPlatformService
.retriveLoanLinkedAssociation(chargeData.getLoanId());
}
final SavingsAccount fromSavingsAccount = null;
final boolean isExceptionForBalanceCheck = false;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(DateUtils.getBusinessLocalDate(),
installmentChargeData.getAmountOutstanding(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN,
portfolioAccountData.accountId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null, null,
LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(),
installmentChargeData.getInstallmentNumber(), AccountTransferType.CHARGE_PAYMENT.getValue(), null, null,
null, null, null, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck);
transferFeeCharge(accountTransferDTO, errors);
}
}
} else if (chargeData.getDueDate() != null && !chargeData.getDueDate().isAfter(DateUtils.getBusinessLocalDate())) {
final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService
.retriveLoanLinkedAssociation(chargeData.getLoanId());
final SavingsAccount fromSavingsAccount = null;
final boolean isExceptionForBalanceCheck = false;
final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(DateUtils.getBusinessLocalDate(),
chargeData.getAmountOutstanding(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN,
portfolioAccountData.accountId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null, null,
LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(), null,
AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, null, null, null, fromSavingsAccount,
isRegularTransaction, isExceptionForBalanceCheck);
transferFeeCharge(accountTransferDTO, errors);
}
}
}
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
}
private void transferFeeCharge(final AccountTransferDTO accountTransferDTO, List<Throwable> errors) {
try {
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} catch (RuntimeException e) {
log.error("Exception while paying charge {} for loan id {}", accountTransferDTO.getChargeId(),
accountTransferDTO.getToAccountId(), e);
errors.add(e);
}
}
private LoanCharge retrieveLoanChargeBy(final Long loanId, final Long loanChargeId) {
final LoanCharge loanCharge = this.loanChargeRepository.findById(loanChargeId)
.orElseThrow(() -> new LoanChargeNotFoundException(loanChargeId));
@@ -2524,50 +2458,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.loanRepositoryWrapper.save(loansToUpdate);
}
@Transactional
@Override
@CronTarget(jobName = JobName.APPLY_HOLIDAYS_TO_LOANS)
public void applyHolidaysToLoans() {
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
if (!isHolidayEnabled) {
return;
}
final Collection<Integer> loanStatuses = new ArrayList<>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(),
LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue()));
// Get all Holidays which are active and not processed
final List<Holiday> holidays = this.holidayRepository.findUnprocessed();
// Loop through all holidays
for (final Holiday holiday : holidays) {
// All offices to which holiday is applied
final Set<Office> offices = holiday.getOffices();
final Collection<Long> officeIds = new ArrayList<>(offices.size());
for (final Office office : offices) {
officeIds.add(office.getId());
}
// get all loans
final List<Loan> loans = new ArrayList<>();
// get all individual and jlg loans
loans.addAll(this.loanRepositoryWrapper.findByClientOfficeIdsAndLoanStatus(officeIds, loanStatuses));
// FIXME: AA optimize to get all client and group loans belongs to a
// office id
// get all group loans
loans.addAll(this.loanRepositoryWrapper.findByGroupOfficeIdsAndLoanStatus(officeIds, loanStatuses));
for (final Loan loan : loans) {
// apply holiday
loan.applyHolidayToRepaymentScheduleDates(holiday, this.loanUtilService);
}
this.loanRepositoryWrapper.save(loans);
holiday.processed();
}
this.holidayRepository.save(holidays);
}
private void checkClientOrGroupActive(final Loan loan) {
final Client client = loan.client();
if (client != null) {
@@ -0,0 +1,61 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.jobs.applyannualfeeforsavings;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ApplyAnnualFeeForSavingsConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService;
@Autowired
private SavingsAccountWritePlatformService savingsAccountWritePlatformService;
@Bean
protected Step applyAnnualFeeForSavingsStep() {
return steps.get(JobName.APPLY_ANNUAL_FEE_FOR_SAVINGS.name()).tasklet(applyAnnualFeeForSavingsTasklet()).build();
}
@Bean
public Job applyAnnualFeeForSavingsJob() {
return jobs.get(JobName.APPLY_ANNUAL_FEE_FOR_SAVINGS.name()).start(applyAnnualFeeForSavingsStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public ApplyAnnualFeeForSavingsTasklet applyAnnualFeeForSavingsTasklet() {
return new ApplyAnnualFeeForSavingsTasklet(savingsAccountChargeReadPlatformService, savingsAccountWritePlatformService);
}
}
@@ -0,0 +1,65 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.jobs.applyannualfeeforsavings;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.savings.data.SavingsAccountAnnualFeeData;
import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class ApplyAnnualFeeForSavingsTasklet implements Tasklet {
private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService;
private final SavingsAccountWritePlatformService savingsAccountWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
final Collection<SavingsAccountAnnualFeeData> annualFeeData = savingsAccountChargeReadPlatformService
.retrieveChargesWithAnnualFeeDue();
for (final SavingsAccountAnnualFeeData savingsAccountReference : annualFeeData) {
try {
savingsAccountWritePlatformService.applyAnnualFee(savingsAccountReference.getId(), savingsAccountReference.getAccountId());
} catch (final PlatformApiDataValidationException e) {
final List<ApiParameterError> errors = e.getErrors();
for (final ApiParameterError error : errors) {
log.error("Apply annual fee failed for account: {} with message {}", savingsAccountReference.getAccountNo(), error);
}
} catch (final Exception ex) {
log.error("Apply annual fee failed for account: {}", savingsAccountReference.getAccountNo(), ex);
}
}
log.info("{}: Records affected by applyAnnualFeeForSavings: {}", ThreadLocalContextUtil.getTenant().getName(),
annualFeeData.size());
return RepeatStatus.FINISHED;
}
}
@@ -0,0 +1,60 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.jobs.generateadhocclientschhedule;
import org.apache.fineract.adhocquery.service.AdHocReadPlatformService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class GenerateAdhocClientScheduleConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Autowired
private AdHocReadPlatformService adHocReadPlatformService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Bean
protected Step generateAdhocClientScheduleStep() {
return steps.get(JobName.GENERATE_ADHOC_CLIENT_SCHEDULE.name()).tasklet(generateAdhocClientScheduleTasklet()).build();
}
@Bean
public Job generateAdhocClientScheduleJob() {
return jobs.get(JobName.GENERATE_ADHOC_CLIENT_SCHEDULE.name()).start(generateAdhocClientScheduleStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public GenerateAdhocClientScheduleTasklet generateAdhocClientScheduleTasklet() {
return new GenerateAdhocClientScheduleTasklet(adHocReadPlatformService, jdbcTemplate);
}
}

Some files were not shown because too many files have changed in this diff Show More