[FINERACT-1678] Inline COB API

This commit is contained in:
taskain7
2022-10-18 19:14:38 +02:00
committed by Arnold Gálovics
parent 22bcccb17b
commit ebe87433e7
53 changed files with 1489 additions and 206 deletions
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.data;
import java.util.List;
import lombok.Data;
@Data
public class LoanIdsResponseDTO {
private List<Long> loanIds;
}
@@ -0,0 +1,26 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.exceptions;
public class LoanAccountLockCannotBeOverruledException extends RuntimeException {
public LoanAccountLockCannotBeOverruledException(String message) {
super(message);
}
}
@@ -44,7 +44,7 @@ import org.springframework.transaction.support.TransactionTemplate;
@Slf4j
@RequiredArgsConstructor
public class LoanItemListener {
public abstract class AbstractLoanItemListener {
private final LoanAccountLockRepository accountLockRepository;
@@ -57,8 +57,7 @@ public class LoanItemListener {
@Override
protected void doInTransactionWithoutResult(@NotNull TransactionStatus status) {
for (Long loanId : loanIds) {
Optional<LoanAccountLock> loanAccountLock = accountLockRepository.findByLoanIdAndLockOwner(loanId,
LockOwner.LOAN_COB_CHUNK_PROCESSING);
Optional<LoanAccountLock> loanAccountLock = accountLockRepository.findByLoanIdAndLockOwner(loanId, getLockOwner());
loanAccountLock.ifPresent(
accountLock -> accountLock.setError(String.format(msg, loanId), ThrowableSerialization.serialize(e)));
}
@@ -101,4 +100,7 @@ public class LoanItemListener {
public void onSkipInWrite(@NotNull Loan item, @NotNull Exception e) {
log.warn("Skipping was triggered during writing of Loan (id={})", item.getId());
}
protected abstract LockOwner getLockOwner();
}
@@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.listener;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
import org.apache.fineract.cob.domain.LockOwner;
import org.springframework.transaction.support.TransactionTemplate;
public class ChunkProcessingLoanItemListener extends AbstractLoanItemListener {
public ChunkProcessingLoanItemListener(LoanAccountLockRepository accountLockRepository, TransactionTemplate transactionTemplate) {
super(accountLockRepository, transactionTemplate);
}
@Override
protected LockOwner getLockOwner() {
return LockOwner.LOAN_COB_CHUNK_PROCESSING;
}
}
@@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.listener;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
import org.apache.fineract.cob.domain.LockOwner;
import org.springframework.transaction.support.TransactionTemplate;
public class InlineCOBLoanItemListener extends AbstractLoanItemListener {
public InlineCOBLoanItemListener(LoanAccountLockRepository accountLockRepository, TransactionTemplate transactionTemplate) {
super(accountLockRepository, transactionTemplate);
}
@Override
protected LockOwner getLockOwner() {
return LockOwner.LOAN_INLINE_COB_PROCESSING;
}
}
@@ -0,0 +1,56 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.loan;
import java.util.TreeMap;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.COBBusinessStepService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.jetbrains.annotations.NotNull;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.AfterStep;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemProcessor;
@RequiredArgsConstructor
@Slf4j
public abstract class AbstractLoanItemProcessor implements ItemProcessor<Loan, Loan> {
private final COBBusinessStepService cobBusinessStepService;
@Setter(AccessLevel.PROTECTED)
private ExecutionContext executionContext;
@Override
public Loan process(@NotNull Loan item) throws Exception {
TreeMap<Long, String> businessStepMap = (TreeMap<Long, String>) executionContext.get(LoanCOBConstant.BUSINESS_STEP_MAP);
return cobBusinessStepService.run(businessStepMap, item);
}
@AfterStep
public ExitStatus afterStep(@NotNull StepExecution stepExecution) {
return ExitStatus.COMPLETED;
}
}
@@ -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.cob.loan;
import java.util.List;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.exceptions.LoanAccountWasAlreadyLocked;
import org.apache.fineract.cob.exceptions.LoanReadException;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.jetbrains.annotations.NotNull;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.AfterStep;
import org.springframework.batch.item.ItemReader;
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractLoanItemReader implements ItemReader<Loan> {
private final LoanRepository loanRepository;
@Setter(AccessLevel.PROTECTED)
private List<Long> alreadyLockedAccounts;
@Setter(AccessLevel.PROTECTED)
private List<Long> remainingData;
private Long loanId;
@Override
public Loan read() throws Exception {
try {
if (remainingData.size() > 0) {
loanId = remainingData.remove(0);
if (alreadyLockedAccounts != null && alreadyLockedAccounts.remove(loanId)) {
throw new LoanAccountWasAlreadyLocked(loanId);
}
return loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
}
} catch (Exception e) {
throw new LoanReadException(loanId, e);
}
return null;
}
@AfterStep
public ExitStatus afterStep(@NotNull StepExecution stepExecution) {
return ExitStatus.COMPLETED;
}
}
@@ -0,0 +1,46 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.loan;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
import org.apache.fineract.cob.domain.LockOwner;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.jetbrains.annotations.NotNull;
import org.springframework.batch.item.data.RepositoryItemWriter;
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractLoanItemWriter extends RepositoryItemWriter<Loan> {
private final LoanAccountLockRepository accountLockRepository;
@Override
public void write(@NotNull List<? extends Loan> items) throws Exception {
super.write(items);
List<Long> loanIds = items.stream().map(AbstractPersistableCustom::getId).toList();
accountLockRepository.deleteByLoanIdInAndLockOwner(loanIds, getLockOwner());
}
protected abstract LockOwner getLockOwner();
}
@@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.loan;
import org.apache.fineract.cob.COBBusinessStepService;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.BeforeStep;
public class InlineCOBLoanItemProcessor extends AbstractLoanItemProcessor {
public InlineCOBLoanItemProcessor(COBBusinessStepService cobBusinessStepService) {
super(cobBusinessStepService);
}
@BeforeStep
public void beforeStep(StepExecution stepExecution) {
setExecutionContext(stepExecution.getJobExecution().getExecutionContext());
}
}
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.loan;
import java.util.ArrayList;
import java.util.List;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.jetbrains.annotations.NotNull;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.BeforeStep;
import org.springframework.batch.item.ExecutionContext;
public class InlineCOBLoanItemReader extends AbstractLoanItemReader {
public InlineCOBLoanItemReader(LoanRepository loanRepository) {
super(loanRepository);
}
@BeforeStep
@SuppressWarnings({ "unchecked" })
public void beforeStep(@NotNull StepExecution stepExecution) {
ExecutionContext executionContext = stepExecution.getJobExecution().getExecutionContext();
List<Long> loanIds = (List<Long>) executionContext.get(LoanCOBConstant.LOAN_IDS);
setRemainingData(new ArrayList<>(loanIds));
}
}
@@ -0,0 +1,34 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.loan;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
import org.apache.fineract.cob.domain.LockOwner;
public class InlineCOBLoanItemWriter extends AbstractLoanItemWriter {
public InlineCOBLoanItemWriter(LoanAccountLockRepository accountLockRepository) {
super(accountLockRepository);
}
@Override
protected LockOwner getLockOwner() {
return LockOwner.LOAN_INLINE_COB_PROCESSING;
}
}
@@ -0,0 +1,73 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.loan;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.COBBusinessStepService;
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameter;
import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository;
import org.apache.fineract.infrastructure.springbatch.SpringBatchJobConstants;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
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.beans.factory.InitializingBean;
@Slf4j
@RequiredArgsConstructor
public class InlineLoanCOBBuildExecutionContextTasklet implements Tasklet, InitializingBean {
private final GoogleGsonSerializerHelper gsonFactory;
private final COBBusinessStepService cobBusinessStepService;
private final CustomJobParameterRepository customJobParameterRepository;
private Gson gson;
@Override
public void afterPropertiesSet() throws Exception {
this.gson = gsonFactory.createSimpleGson();
}
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
TreeMap<Long, String> cobBusinessStepMap = cobBusinessStepService.getCOBBusinessStepMap(LoanCOBBusinessStep.class,
LoanCOBConstant.LOAN_COB_JOB_NAME);
contribution.getStepExecution().getExecutionContext().put(LoanCOBConstant.LOAN_IDS, getLoanIdsFromJobParameters(chunkContext));
contribution.getStepExecution().getExecutionContext().put(LoanCOBConstant.BUSINESS_STEP_MAP, cobBusinessStepMap);
return RepeatStatus.FINISHED;
}
private List<Long> getLoanIdsFromJobParameters(ChunkContext chunkContext) {
Long customJobParameterId = (Long) chunkContext.getStepContext().getJobParameters()
.get(SpringBatchJobConstants.CUSTOM_JOB_PARAMETER_ID_KEY);
CustomJobParameter customJobParameter = customJobParameterRepository.findById(customJobParameterId)
.orElseThrow(() -> new LoanNotFoundException(customJobParameterId));
String parameterJson = customJobParameter.getParameterJson();
return gson.fromJson(parameterJson, new TypeToken<ArrayList<Long>>() {}.getType());
}
}
@@ -27,6 +27,7 @@ public final class LoanCOBConstant {
public static final String LOAN_COB_WORKER_STEP = "loanCOBWorkerStep";
public static final String ALREADY_LOCKED_LOAN_IDS = "alreadyLockedLoanIds";
public static final String INLINE_LOAN_COB_JOB_NAME = "INLINE_LOAN_COB";
private LoanCOBConstant() {
@@ -21,7 +21,7 @@ package org.apache.fineract.cob.loan;
import org.apache.fineract.cob.COBBusinessStepService;
import org.apache.fineract.cob.common.InitialisationTasklet;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
import org.apache.fineract.cob.listener.LoanItemListener;
import org.apache.fineract.cob.listener.ChunkProcessingLoanItemListener;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.springbatch.PropertyService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -105,8 +105,8 @@ public class LoanCOBWorkerConfiguration {
}
@Bean
public LoanItemListener loanItemListener() {
return new LoanItemListener(accountLockRepository, transactionTemplate);
public ChunkProcessingLoanItemListener loanItemListener() {
return new ChunkProcessingLoanItemListener(accountLockRepository, transactionTemplate);
}
@Bean
@@ -0,0 +1,119 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.loan;
import org.apache.fineract.cob.COBBusinessStepService;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
import org.apache.fineract.cob.listener.InlineCOBLoanItemListener;
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.infrastructure.springbatch.PropertyService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
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.batch.core.listener.ExecutionContextPromotionListener;
import org.springframework.batch.integration.config.annotation.EnableBatchIntegration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.support.TransactionTemplate;
@Configuration
@EnableBatchIntegration
public class LoanInlineCOBConfig {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Autowired
private PropertyService propertyService;
@Autowired
private LoanRepository loanRepository;
@Autowired
private COBBusinessStepService cobBusinessStepService;
@Autowired
private LoanAccountLockRepository accountLockRepository;
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private CustomJobParameterRepository loanIdListRepository;
@Autowired
private GoogleGsonSerializerHelper googleGsonSerializerHelper;
@Bean
public InlineLoanCOBBuildExecutionContextTasklet inlineLoanCOBBuildExecutionContextTasklet() {
return new InlineLoanCOBBuildExecutionContextTasklet(googleGsonSerializerHelper, cobBusinessStepService, loanIdListRepository);
}
@Bean
protected Step inlineCOBBuildExecutionContextStep() {
return stepBuilderFactory.get("Inline COB build execution context step").tasklet(inlineLoanCOBBuildExecutionContextTasklet())
.listener(inlineCobPromotionListener()).build();
}
@Bean
public Step inlineLoanCOBStep() {
return stepBuilderFactory.get("Inline Loan COB Step").<Loan, Loan>chunk(propertyService.getChunkSize(JobName.LOAN_COB.name()))
.reader(inlineCobWorkerItemReader()).processor(inlineCobWorkerItemProcessor()).writer(inlineCobWorkerItemWriter())
.listener(inlineCobLoanItemListener()).build();
}
@Bean(name = "loanInlineCOBJob")
public Job loanInlineCOBJob() {
return jobBuilderFactory.get(LoanCOBConstant.INLINE_LOAN_COB_JOB_NAME) //
.start(inlineCOBBuildExecutionContextStep()).next(inlineLoanCOBStep()) //
.incrementer(new RunIdIncrementer()) //
.build();
}
@Bean
public InlineCOBLoanItemReader inlineCobWorkerItemReader() {
return new InlineCOBLoanItemReader(loanRepository);
}
@Bean
public InlineCOBLoanItemProcessor inlineCobWorkerItemProcessor() {
return new InlineCOBLoanItemProcessor(cobBusinessStepService);
}
@Bean
public InlineCOBLoanItemWriter inlineCobWorkerItemWriter() {
InlineCOBLoanItemWriter repositoryItemWriter = new InlineCOBLoanItemWriter(accountLockRepository);
repositoryItemWriter.setRepository(loanRepository);
return repositoryItemWriter;
}
@Bean
public InlineCOBLoanItemListener inlineCobLoanItemListener() {
return new InlineCOBLoanItemListener(accountLockRepository, transactionTemplate);
}
@Bean
public ExecutionContextPromotionListener inlineCobPromotionListener() {
ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();
listener.setKeys(new String[] { LoanCOBConstant.LOAN_IDS, LoanCOBConstant.BUSINESS_STEP_MAP });
return listener;
}
}
@@ -18,42 +18,18 @@
*/
package org.apache.fineract.cob.loan;
import java.util.TreeMap;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.COBBusinessStepService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.jetbrains.annotations.NotNull;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.AfterStep;
import org.springframework.batch.core.annotation.BeforeStep;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemProcessor;
@RequiredArgsConstructor
@Slf4j
public class LoanItemProcessor implements ItemProcessor<Loan, Loan> {
public class LoanItemProcessor extends AbstractLoanItemProcessor {
private final COBBusinessStepService cobBusinessStepService;
private StepExecution stepExecution;
public LoanItemProcessor(COBBusinessStepService cobBusinessStepService) {
super(cobBusinessStepService);
}
@BeforeStep
public void beforeStep(@NotNull StepExecution stepExecution) {
this.stepExecution = stepExecution;
public void beforeStep(StepExecution stepExecution) {
setExecutionContext(stepExecution.getExecutionContext());
}
@Override
public Loan process(@NotNull Loan item) throws Exception {
ExecutionContext executionContext = stepExecution.getExecutionContext();
TreeMap<Long, String> businessStepMap = (TreeMap<Long, String>) executionContext.get(LoanCOBConstant.BUSINESS_STEP_MAP);
return cobBusinessStepService.run(businessStepMap, item);
}
@AfterStep
public ExitStatus afterStep(@NotNull StepExecution stepExecution) {
return ExitStatus.COMPLETED;
}
}
@@ -20,59 +20,26 @@ package org.apache.fineract.cob.loan;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.exceptions.LoanAccountWasAlreadyLocked;
import org.apache.fineract.cob.exceptions.LoanReadException;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.jetbrains.annotations.NotNull;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.AfterStep;
import org.springframework.batch.core.annotation.BeforeStep;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemReader;
@Slf4j
@RequiredArgsConstructor
public class LoanItemReader implements ItemReader<Loan> {
public class LoanItemReader extends AbstractLoanItemReader {
private final LoanRepository loanRepository;
private List<Long> alreadyLockedAccounts;
private List<Long> remainingData;
private Long loanId;
public LoanItemReader(LoanRepository loanRepository) {
super(loanRepository);
}
@BeforeStep
@SuppressWarnings({ "unchecked" })
public void beforeStep(@NotNull StepExecution stepExecution) {
ExecutionContext executionContext = stepExecution.getExecutionContext();
ExecutionContext jobExecutionContext = stepExecution.getJobExecution().getExecutionContext();
List<Long> loanIds = (List<Long>) executionContext.get(LoanCOBConstant.LOAN_IDS);
alreadyLockedAccounts = (List<Long>) jobExecutionContext.get(LoanCOBConstant.ALREADY_LOCKED_LOAN_IDS);
remainingData = new ArrayList<>(loanIds);
}
@Override
public Loan read() throws Exception {
try {
if (remainingData.size() > 0) {
loanId = remainingData.remove(0);
if (alreadyLockedAccounts != null && alreadyLockedAccounts.remove(loanId)) {
throw new LoanAccountWasAlreadyLocked(loanId);
}
return loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
}
} catch (Exception e) {
throw new LoanReadException(loanId, e);
}
return null;
}
@AfterStep
public ExitStatus afterStep(@NotNull StepExecution stepExecution) {
return ExitStatus.COMPLETED;
setAlreadyLockedAccounts((List<Long>) jobExecutionContext.get(LoanCOBConstant.ALREADY_LOCKED_LOAN_IDS));
setRemainingData(new ArrayList<>(loanIds));
}
}
@@ -18,28 +18,17 @@
*/
package org.apache.fineract.cob.loan;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
import org.apache.fineract.cob.domain.LockOwner;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.jetbrains.annotations.NotNull;
import org.springframework.batch.item.data.RepositoryItemWriter;
@Slf4j
@RequiredArgsConstructor
public class LoanItemWriter extends RepositoryItemWriter<Loan> {
public class LoanItemWriter extends AbstractLoanItemWriter {
private final LoanAccountLockRepository accountLockRepository;
@Override
public void write(@NotNull List<? extends Loan> items) throws Exception {
super.write(items);
List<Long> loanIds = items.stream().map(AbstractPersistableCustom::getId).toList();
accountLockRepository.deleteByLoanIdInAndLockOwner(loanIds, LockOwner.LOAN_COB_CHUNK_PROCESSING);
public LoanItemWriter(LoanAccountLockRepository accountLockRepository) {
super(accountLockRepository);
}
@Override
protected LockOwner getLockOwner() {
return LockOwner.LOAN_COB_CHUNK_PROCESSING;
}
}
@@ -18,8 +18,6 @@
*/
package org.apache.fineract.cob.service;
import com.google.common.base.Splitter;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.cob.exceptions.BusinessStepException;
import org.apache.fineract.cob.exceptions.BusinessStepNotBelongsToJobException;
@@ -40,11 +38,6 @@ public class BusinessStepConfigUpdateHandler implements NewCommandSourceHandler
@Override
@Transactional
public CommandProcessingResult processCommand(JsonCommand command) throws BusinessStepNotBelongsToJobException, BusinessStepException {
List<String> split = Splitter.on("/").splitToList(command.getUrl());
String jobName = split.get(split.size() - 2);
if ("null".equals(jobName)) {
jobName = null;
}
return configJobParameterService.updateStepConfigByJobName(command, jobName);
return configJobParameterService.updateStepConfigByJobName(command, command.getJobName());
}
}
@@ -0,0 +1,53 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.service;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class InlineLoanCOBExecutionDataParser {
private final FromJsonHelper jsonHelper;
public List<Long> parseExecution(JsonCommand command) {
JsonObject element = extractJsonObject(command);
String[] loanArray = jsonHelper.extractArrayNamed("loanIds", element);
return Arrays.stream(loanArray).map(Long::parseLong).toList();
}
private JsonObject extractJsonObject(JsonCommand command) {
String json = command.json();
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final JsonElement element = jsonHelper.parse(json);
return element.getAsJsonObject();
}
}
@@ -0,0 +1,177 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cob.service;
import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW;
import com.google.gson.Gson;
import java.util.ArrayList;
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.commons.lang3.StringUtils;
import org.apache.fineract.cob.domain.LoanAccountLock;
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
import org.apache.fineract.cob.domain.LockOwner;
import org.apache.fineract.cob.exceptions.LoanAccountLockCannotBeOverruledException;
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.PlatformInternalServerException;
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameter;
import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository;
import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException;
import org.apache.fineract.infrastructure.jobs.service.InlineExecutorService;
import org.apache.fineract.infrastructure.springbatch.SpringBatchJobConstants;
import org.jetbrains.annotations.NotNull;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameter;
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.batch.core.launch.NoSuchJobException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@Service
@Slf4j
@RequiredArgsConstructor
public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService, InitializingBean {
private static final String JOB_EXECUTION_FAILED_MESSAGE = "Job execution failed for job with name: ";
private final GoogleGsonSerializerHelper gsonFactory;
private final LoanAccountLockRepository loanAccountLockRepository;
private final InlineLoanCOBExecutionDataParser dataParser;
private final JobLauncher jobLauncher;
private final JobLocator jobLocator;
private final JobExplorer jobExplorer;
private final TransactionTemplate transactionTemplate;
private final CustomJobParameterRepository customJobParameterRepository;
private Gson gson;
@Override
public void afterPropertiesSet() throws Exception {
this.gson = gsonFactory.createSimpleGson();
}
@Override
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public CommandProcessingResult executeInlineJob(JsonCommand command, String jobName) throws LoanAccountLockCannotBeOverruledException {
List<Long> loanIds = dataParser.parseExecution(command);
lockLoanAccounts(loanIds);
execute(loanIds, jobName);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).build();
}
private List<LoanAccountLock> getLoanAccountLocks(List<Long> loanIds) {
List<LoanAccountLock> loanAccountLocks = new ArrayList<>();
List<Long> alreadyLockedLoanIds = new ArrayList<>();
loanIds.forEach(loanId -> {
Optional<LoanAccountLock> loanLockOptional = loanAccountLockRepository.findById(loanId);
if (loanLockOptional.isPresent()) {
LoanAccountLock loanAccountLock = loanLockOptional.get();
if (isLockOverrulable(loanAccountLock)) {
loanAccountLocks.add(loanAccountLock);
} else {
alreadyLockedLoanIds.add(loanId);
}
} else {
loanAccountLocks.add(new LoanAccountLock(loanId, LockOwner.LOAN_INLINE_COB_PROCESSING));
}
});
if (!alreadyLockedLoanIds.isEmpty()) {
String message = "There is a hard lock on the loan account without any error, so it can't be overruled.";
String loanIdsMessage = " Locked loan IDs: " + alreadyLockedLoanIds;
throw new LoanAccountLockCannotBeOverruledException(message + loanIdsMessage);
}
return loanAccountLocks;
}
private void execute(List<Long> loanIds, String jobName) {
Job inlineLoanCOBJob;
try {
inlineLoanCOBJob = jobLocator.getJob(jobName);
} catch (NoSuchJobException e) {
throw new JobNotFoundException(jobName, e);
}
JobParameters jobParameters = new JobParametersBuilder(jobExplorer).getNextJobParameters(inlineLoanCOBJob)
.addJobParameters(new JobParameters(getJobParametersMap(loanIds))).toJobParameters();
JobExecution jobExecution;
try {
jobExecution = jobLauncher.run(inlineLoanCOBJob, jobParameters);
} catch (Exception e) {
log.error("{}{}", JOB_EXECUTION_FAILED_MESSAGE, jobName, e);
throw new PlatformInternalServerException("error.msg.sheduler.job.execution.failed", JOB_EXECUTION_FAILED_MESSAGE, jobName, e);
}
if (!BatchStatus.COMPLETED.equals(jobExecution.getStatus())) {
log.error("{}{}", JOB_EXECUTION_FAILED_MESSAGE, jobName);
throw new PlatformInternalServerException("error.msg.sheduler.job.execution.failed", JOB_EXECUTION_FAILED_MESSAGE, jobName);
}
}
private Map<String, JobParameter> getJobParametersMap(List<Long> loanIds) {
// TODO: refactor for a more generic solution
String parameterJson = gson.toJson(loanIds);
CustomJobParameter customJobParameter = new CustomJobParameter();
customJobParameter.setParameterJson(parameterJson);
Long customJobParameterId = customJobParameterRepository.saveAndFlush(customJobParameter).getId();
Map<String, JobParameter> jobParameterMap = new HashMap<>();
jobParameterMap.put(SpringBatchJobConstants.CUSTOM_JOB_PARAMETER_ID_KEY, new JobParameter(customJobParameterId));
return jobParameterMap;
}
private void lockLoanAccounts(List<Long> loanIds) {
transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(@NotNull TransactionStatus status) {
List<LoanAccountLock> loanAccountLocks = getLoanAccountLocks(loanIds);
loanAccountLocks.forEach(loanAccountLock -> {
loanAccountLock.setNewLockOwner(LockOwner.LOAN_INLINE_COB_PROCESSING);
loanAccountLockRepository.save(loanAccountLock);
});
}
});
}
private boolean isLockOverrulable(LoanAccountLock loanAccountLock) {
if (LockOwner.LOAN_COB_PARTITIONING.equals(loanAccountLock.getLockOwner())) {
return true;
} else {
return StringUtils.isNotBlank(loanAccountLock.getError());
}
}
}
@@ -104,6 +104,9 @@ public class CommandSource extends AbstractPersistableCustom {
@Column(name = "organisation_creditbureau_id")
private Long organisationCreditBureauId;
@Column(name = "job_name")
private String jobName;
public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final JsonCommand command, final AppUser maker) {
return new CommandSource(wrapper.actionName(), wrapper.entityName(), wrapper.getHref(), command.entityId(), command.subentityId(),
command.json(), maker);
@@ -138,6 +141,10 @@ public class CommandSource extends AbstractPersistableCustom {
return this.organisationCreditBureauId;
}
public String getJobName() {
return this.jobName;
}
public void setOrganisationCreditBureauId(Long organisationCreditBureauId) {
this.organisationCreditBureauId = organisationCreditBureauId;
}
@@ -40,6 +40,7 @@ public class CommandWrapper {
private final Long productId;
private final Long creditBureauId;
private final Long organisationCreditBureauId;
private final String jobName;
@SuppressWarnings("unused")
private Long templateId;
@@ -80,12 +81,13 @@ public class CommandWrapper {
this.productId = productId;
this.creditBureauId = null;
this.organisationCreditBureauId = null;
this.jobName = null;
}
public CommandWrapper(final Long officeId, final Long groupId, final Long clientId, final Long loanId, final Long savingsId,
final String actionName, final String entityName, final Long entityId, final Long subentityId, final String href,
final String json, final String transactionId, final Long productId, final Long templateId, final Long creditBureauId,
final Long organisationCreditBureauId) {
final Long organisationCreditBureauId, final String jobName) {
this.commandId = null;
this.officeId = officeId;
@@ -105,6 +107,7 @@ public class CommandWrapper {
this.templateId = templateId;
this.creditBureauId = creditBureauId;
this.organisationCreditBureauId = organisationCreditBureauId;
this.jobName = jobName;
}
private CommandWrapper(final Long commandId, final String actionName, final String entityName, final Long resourceId,
@@ -129,6 +132,7 @@ public class CommandWrapper {
this.productId = productId;
this.creditBureauId = creditBureauId;
this.organisationCreditBureauId = organisationCreditBureauId;
this.jobName = null;
}
public Long getCreditBureauId() {
@@ -338,4 +342,8 @@ public class CommandWrapper {
return this.actionName.equalsIgnoreCase("UPDATE") && this.entityName.equalsIgnoreCase("DISBURSEMENTDETAIL")
&& this.entityId == null;
}
public String getJobName() {
return jobName;
}
}
@@ -44,11 +44,12 @@ public class CommandWrapperBuilder {
private Long templateId;
private Long creditBureauId;
private Long organisationCreditBureauId;
private String jobName;
public CommandWrapper build() {
return new CommandWrapper(this.officeId, this.groupId, this.clientId, this.loanId, this.savingsId, this.actionName, this.entityName,
this.entityId, this.subentityId, this.href, this.json, this.transactionId, this.productId, this.templateId,
this.creditBureauId, this.organisationCreditBureauId);
this.creditBureauId, this.organisationCreditBureauId, this.jobName);
}
public CommandWrapperBuilder updateCreditBureau() {
@@ -3562,7 +3563,15 @@ public class CommandWrapperBuilder {
this.actionName = "UPDATE";
this.entityName = "BATCH_BUSINESS_STEP";
this.href = "/jobs/" + jobName + "/steps";
this.jobName = jobName;
return this;
}
public CommandWrapperBuilder executeInlineJob(String jobName) {
this.actionName = "EXECUTE";
this.entityName = "INLINE_JOB";
this.href = "/jobs/" + jobName + "/inline";
this.jobName = jobName;
return this;
}
}
@@ -85,7 +85,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
command = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(), wrapper.getEntityId(),
wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(), wrapper.getSavingsId(),
wrapper.getTransactionId(), wrapper.getHref(), wrapper.getProductId(), wrapper.getCreditBureauId(),
wrapper.getOrganisationCreditBureauId());
wrapper.getOrganisationCreditBureauId(), wrapper.getJobName());
while (numberOfRetries <= maxNumberOfRetries) {
try {
result = this.processAndLogCommandService.executeCommand(wrapper, command, isApprovedByChecker);
@@ -137,7 +137,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
commandSourceInput.subresourceId(), commandSourceInput.getGroupId(), commandSourceInput.getClientId(),
commandSourceInput.getLoanId(), commandSourceInput.getSavingsId(), commandSourceInput.getTransactionId(),
commandSourceInput.getResourceGetUrl(), commandSourceInput.getProductId(), commandSourceInput.getCreditBureauId(),
commandSourceInput.getOrganisationCreditBureauId());
commandSourceInput.getOrganisationCreditBureauId(), commandSourceInput.getJobName());
final boolean makerCheckerApproval = true;
return this.processAndLogCommandService.executeCommand(wrapper, command, makerCheckerApproval);
@@ -262,7 +262,7 @@ public class CenterImportHandler implements ImportHandler {
String payload = gsonBuilder.create().toJson(calendarData);
CommandWrapper commandWrapper = new CommandWrapper(result.getOfficeId(), result.getGroupId(), result.getClientId(),
result.getLoanId(), result.getSavingsId(), null, null, null, null, null, payload, result.getTransactionId(),
result.getProductId(), null, null, null);
result.getProductId(), null, null, null, null);
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.createCalendar(commandWrapper, TemplatePopulateImportConstants.CENTER_ENTITY_TYPE, result.getGroupId()) //
.withJson(payload) //
@@ -241,7 +241,7 @@ public class GroupImportHandler implements ImportHandler {
String payload = gsonBuilder.create().toJson(calendarData);
CommandWrapper commandWrapper = new CommandWrapper(result.getOfficeId(), result.getGroupId(), result.getClientId(),
result.getLoanId(), result.getSavingsId(), null, null, null, null, null, payload, result.getTransactionId(),
result.getProductId(), null, null, null);
result.getProductId(), null, null, null, null);
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.createCalendar(commandWrapper, TemplatePopulateImportConstants.CENTER_ENTITY_TYPE, result.getGroupId()) //
.withJson(payload) //
@@ -64,29 +64,31 @@ public final class JsonCommand {
private final Long productId;
private final Long creditBureauId;
private final Long organisationCreditBureauId;
private final String jobName;
public static JsonCommand from(final String jsonCommand, final JsonElement parsedCommand, final FromJsonHelper fromApiJsonHelper,
final String entityName, final Long resourceId, final Long subresourceId, final Long groupId, final Long clientId,
final Long loanId, final Long savingsId, final String transactionId, final String url, final Long productId,
final Long creditBureauId, final Long organisationCreditBureauId) {
final Long creditBureauId, final Long organisationCreditBureauId, final String jobName) {
return new JsonCommand(null, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, groupId,
clientId, loanId, savingsId, transactionId, url, productId, creditBureauId, organisationCreditBureauId);
clientId, loanId, savingsId, transactionId, url, productId, creditBureauId, organisationCreditBureauId, jobName);
}
public static JsonCommand fromExistingCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand,
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId,
final String url, final Long productId, final Long creditBureauId, final Long organisationCreditBureauId) {
final String url, final Long productId, final Long creditBureauId, final Long organisationCreditBureauId,
final String jobName) {
return new JsonCommand(commandId, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, null, null,
null, null, null, url, productId, creditBureauId, organisationCreditBureauId);
null, null, null, url, productId, creditBureauId, organisationCreditBureauId, jobName);
}
public static JsonCommand fromExistingCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand,
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId,
final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final String transactionId, final String url,
final Long productId, Long creditBureauId, final Long organisationCreditBureauId) {
final Long productId, Long creditBureauId, final Long organisationCreditBureauId, final String jobName) {
return new JsonCommand(commandId, jsonCommand, parsedCommand, fromApiJsonHelper, entityName, resourceId, subresourceId, groupId,
clientId, loanId, savingsId, transactionId, url, productId, creditBureauId, organisationCreditBureauId);
clientId, loanId, savingsId, transactionId, url, productId, creditBureauId, organisationCreditBureauId, jobName);
}
@@ -94,20 +96,22 @@ public final class JsonCommand {
final String jsonCommand = command.fromApiJsonHelper.toJson(parsedCommand);
return new JsonCommand(command.commandId, jsonCommand, parsedCommand, command.fromApiJsonHelper, command.entityName,
command.resourceId, command.subresourceId, command.groupId, command.clientId, command.loanId, command.savingsId,
command.transactionId, command.url, command.productId, command.creditBureauId, command.organisationCreditBureauId);
command.transactionId, command.url, command.productId, command.creditBureauId, command.organisationCreditBureauId,
command.jobName);
}
public static JsonCommand fromExistingCommand(JsonCommand command, final JsonElement parsedCommand, final Long clientId) {
final String jsonCommand = command.fromApiJsonHelper.toJson(parsedCommand);
return new JsonCommand(command.commandId, jsonCommand, parsedCommand, command.fromApiJsonHelper, command.entityName,
command.resourceId, command.subresourceId, command.groupId, clientId, command.loanId, command.savingsId,
command.transactionId, command.url, command.productId, command.creditBureauId, command.organisationCreditBureauId);
command.transactionId, command.url, command.productId, command.creditBureauId, command.organisationCreditBureauId,
command.jobName);
}
public JsonCommand(final Long commandId, final String jsonCommand, final JsonElement parsedCommand,
final FromJsonHelper fromApiJsonHelper, final String entityName, final Long resourceId, final Long subresourceId,
final Long groupId, final Long clientId, final Long loanId, final Long savingsId, final String transactionId, final String url,
final Long productId, final Long creditBureauId, final Long organisationCreditBureauId) {
final Long productId, final Long creditBureauId, final Long organisationCreditBureauId, final String jobName) {
this.commandId = commandId;
this.jsonCommand = jsonCommand;
@@ -125,6 +129,7 @@ public final class JsonCommand {
this.productId = productId;
this.creditBureauId = creditBureauId;
this.organisationCreditBureauId = organisationCreditBureauId;
this.jobName = jobName;
}
public static JsonCommand fromJsonElement(final Long resourceId, final JsonElement parsedCommand) {
@@ -153,6 +158,7 @@ public final class JsonCommand {
this.productId = null;
this.creditBureauId = null;
this.organisationCreditBureauId = null;
this.jobName = null;
}
public JsonCommand(final Long resourceId, final JsonElement parsedCommand, final FromJsonHelper fromApiJsonHelper) {
@@ -172,10 +178,11 @@ public final class JsonCommand {
this.productId = null;
this.creditBureauId = null;
this.organisationCreditBureauId = null;
this.jobName = null;
}
public static JsonCommand from(final String jsonCommand) {
return new JsonCommand(null, jsonCommand, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
return new JsonCommand(null, jsonCommand, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
}
@@ -255,6 +262,10 @@ public final class JsonCommand {
return this.productId;
}
public String getJobName() {
return this.jobName;
}
private boolean differenceExists(final TemporalAccessor baseValue, final TemporalAccessor workingCopyValue) {
boolean differenceExists = false;
@@ -0,0 +1,45 @@
/**
* 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.core.exceptionmapper;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.cob.exceptions.LoanAccountLockCannotBeOverruledException;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.springframework.stereotype.Component;
@Provider
@Component
@Slf4j
public class LoanAccountLockCannotBeOverruledExceptionMapper implements ExceptionMapper<LoanAccountLockCannotBeOverruledException> {
@Override
public Response toResponse(LoanAccountLockCannotBeOverruledException exception) {
final String globalisationMessageCode = "error.msg.invalid.request.body";
final String defaultUserMessage = exception.getMessage();
log.warn("Exception: {}, Message: {}", exception.getClass().getName(), defaultUserMessage);
final ApiParameterError error = ApiParameterError.generalError(globalisationMessageCode, defaultUserMessage);
return Response.status(Response.Status.CONFLICT).entity(error).type(MediaType.APPLICATION_JSON).build();
}
}
@@ -0,0 +1,45 @@
/**
* 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.core.exceptionmapper;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.springframework.stereotype.Component;
@Provider
@Component
@Slf4j
public class LoanNotFoundExceptionMapper implements ExceptionMapper<LoanNotFoundException> {
@Override
public Response toResponse(LoanNotFoundException exception) {
final String globalisationMessageCode = "error.msg.invalid.request.body";
final String defaultUserMessage = exception.getMessage();
log.warn("Exception: {}, Message: {}", exception.getClass().getName(), defaultUserMessage);
final ApiParameterError error = ApiParameterError.generalError(globalisationMessageCode, defaultUserMessage);
return Response.status(Response.Status.NOT_FOUND).entity(error).type(MediaType.APPLICATION_JSON).build();
}
}
@@ -96,6 +96,10 @@ public final class GoogleGsonSerializerHelper {
return serializer.toJson(singleDataObject);
}
public Gson createSimpleGson() {
return createGsonBuilder().create();
}
public static GsonBuilder createGsonBuilder() {
return createGsonBuilder(false);
}
@@ -450,7 +450,7 @@ public class ThitsaWorksCreditBureauIntegrationWritePlatformServiceImpl implemen
apicommand = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(), wrapper.getEntityId(),
wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(), wrapper.getSavingsId(),
wrapper.getTransactionId(), wrapper.getHref(), wrapper.getProductId(), wrapper.getCreditBureauId(),
wrapper.getOrganisationCreditBureauId());
wrapper.getOrganisationCreditBureauId(), wrapper.getJobName());
this.fromApiJsonDeserializer.validateForCreate(apicommand.json());
@@ -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.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.cob.data.LoanIdsResponseDTO;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.springframework.stereotype.Component;
@Path("/jobs")
@Component
@Tag(name = "Inline Job", description = "")
@RequiredArgsConstructor
public class InlineJobResource {
private final PortfolioCommandSourceWritePlatformService commandWritePlatformService;
private final DefaultToApiJsonSerializer<LoanIdsResponseDTO> serializer;
@POST
@Path("{jobName}/inline")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Starts an inline Job", description = "Starts an inline Job")
@RequestBody(content = @Content(schema = @Schema(implementation = InlineJobResourceSwagger.InlineJobRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = InlineJobResourceSwagger.InlineJobResponse.class)))) })
public String executeInlineJob(@PathParam("jobName") @Parameter(description = "jobName") final String jobName,
@Parameter(hidden = true) final String jsonRequestBody) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().executeInlineJob(jobName).withJson(jsonRequestBody).build();
CommandProcessingResult result = commandWritePlatformService.logCommandSource(commandRequest);
return serializer.serialize(result);
}
}
@@ -0,0 +1,44 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.jobs.api;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
final class InlineJobResourceSwagger {
private InlineJobResourceSwagger() {}
@Schema(description = "InlineJobRequest")
public static final class InlineJobRequest {
private InlineJobRequest() {}
public List<Long> loanIds;
}
@Schema(description = "InlineJobResponse")
public static final class InlineJobResponse {
private InlineJobResponse() {}
public List<Long> loanIds;
}
}
@@ -0,0 +1,38 @@
/**
* 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.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
@Entity
@Table(name = "batch_custom_job_parameters")
@NoArgsConstructor
@Getter
@Setter
public class CustomJobParameter extends AbstractPersistableCustom {
@Column(name = "parameter_json", nullable = false)
private String parameterJson;
}
@@ -0,0 +1,23 @@
/**
* 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.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CustomJobParameterRepository extends JpaRepository<CustomJobParameter, Long> {}
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.jobs.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.batch.core.launch.NoSuchJobException;
import org.springframework.dao.EmptyResultDataAccessException;
/**
@@ -30,6 +31,10 @@ public class JobNotFoundException extends AbstractPlatformResourceNotFoundExcept
super("error.msg.sheduler.job.id.invalid", "Job with identifier " + identifier + " does not exist", identifier);
}
public JobNotFoundException(final String identifier, NoSuchJobException e) {
super("error.msg.sheduler.job.id.invalid", "Job with identifier " + identifier + " does not exist", identifier, e);
}
public JobNotFoundException(String identifier, EmptyResultDataAccessException e) {
super("error.msg.sheduler.job.id.invalid", "Job with identifier " + identifier + " does not exist", identifier, e);
}
@@ -0,0 +1,27 @@
/**
* 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 org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface InlineExecutorService {
CommandProcessingResult executeInlineJob(JsonCommand command, String jobName);
}
@@ -0,0 +1,42 @@
/**
* 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 lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
@CommandType(entity = "INLINE_JOB", action = "EXECUTE")
public class InlineJobExecuteHandler implements NewCommandSourceHandler {
private final ApplicationContext applicationContext;
@Override
public CommandProcessingResult processCommand(JsonCommand command) {
InlineJobType inlineJobType = InlineJobType.getInlineJobType(command.getJobName());
InlineExecutorService inlineJobExecutorService = applicationContext.getBean(inlineJobType.getExecutorServiceClass());
return inlineJobExecutorService.executeInlineJob(command, inlineJobType.getInlineJobName());
}
}
@@ -0,0 +1,43 @@
/**
* 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.Arrays;
import java.util.Optional;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.cob.service.InlineLoanCOBExecutorServiceImpl;
@RequiredArgsConstructor
public enum InlineJobType {
LOAN_COB("LOAN_COB", "INLINE_LOAN_COB", InlineLoanCOBExecutorServiceImpl.class);
private final String jobName;
@Getter
private final String inlineJobName;
@Getter
private final Class<? extends InlineExecutorService> executorServiceClass;
public static InlineJobType getInlineJobType(String jobName) {
Optional<InlineJobType> optionalInlineJobType = Arrays.stream(InlineJobType.values())
.filter(inlineCOBType -> jobName.equals(inlineCOBType.jobName)).findAny();
return optionalInlineJobType.orElseThrow(() -> new IllegalArgumentException("Inline Job is not found by job name: " + jobName));
}
}
@@ -0,0 +1,24 @@
/**
* 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.springbatch;
public abstract class SpringBatchJobConstants {
public static final String CUSTOM_JOB_PARAMETER_ID_KEY = "CUSTOM_JOB_PARAMETER_ID";
}
@@ -37,7 +37,7 @@ public class InteropWrapperBuilder {
public CommandWrapper build() {
return new CommandWrapper(null, null, null, null, null, actionName, entityName, null, null, href, json, null, null, null, null,
null);
null, null);
}
public InteropWrapperBuilder withJson(final String json) {
@@ -2857,7 +2857,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(overdueInstallment.toString());
final JsonCommand command = JsonCommand.from(overdueInstallment.toString(), parsedCommand, this.fromApiJsonHelper, null, null,
null, null, null, loanId, null, null, null, null, null, null);
null, null, null, loanId, null, null, null, null, null, null, null);
LoanOverdueDTO overdueDTO = applyChargeToOverdueLoanInstallment(loan, overdueInstallment.getChargeId(),
overdueInstallment.getPeriodNumber(), command);
loan = overdueDTO.getLoan();
@@ -46,7 +46,7 @@ public class ShareAccountCommandsServiceImpl implements AccountsCommandsService
public Object handleCommand(Long accountId, String command, String jsonBody) {
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(jsonBody);
final JsonCommand jsonCommand = JsonCommand.from(jsonBody, parsedCommand, this.fromApiJsonHelper, null, null, null, null, null,
null, null, null, null, null, null, null);
null, null, null, null, null, null, null, null);
if (ShareAccountApiConstants.APPROVE_COMMAND.equals(command)) {
return approveShareAccount(accountId, jsonCommand);
}
@@ -45,7 +45,7 @@ public class ShareProductCommandsServiceImpl implements ProductCommandsService {
public Object handleCommand(Long productId, String command, String jsonBody) {
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(jsonBody);
final JsonCommand jsonCommand = JsonCommand.from(jsonBody, parsedCommand, this.fromApiJsonHelper, null, null, null, null, null,
null, null, null, null, null, null, null);
null, null, null, null, null, null, null, null);
if (ShareProductApiConstants.PREIEW_DIVIDENDS_COMMAND_STRING.equals(command)) {
return null;
} else if (ShareProductApiConstants.POST_DIVIDENdS_COMMAND_STRING.equals(command)) {
@@ -77,4 +77,7 @@
<include file="parts/0055_add_submitted_on_date_to_loan_charge.xml" relativeToChangelogFile="true"/>
<include file="parts/0056_add_external_event_default_configuration.xml" relativeToChangelogFile="true"/>
<include file="parts/0057_add_principal_adjustments_to_loan.xml" relativeToChangelogFile="true"/>
<include file="parts/0058_add_execute_inline_cob_permission.xml" relativeToChangelogFile="true"/>
<include file="parts/0059_add_spring_batch_loan_id_list_table.xml" relativeToChangelogFile="true"/>
<include file="parts/0060_add_job_name_to_command_source.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="EXECUTE_INLINE_JOB"/>
<column name="entity_name" value="INLINE_JOB"/>
<column name="action_name" value="EXECUTE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<createTable tableName="batch_custom_job_parameters">
<column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="parameter_json" type="TEXT">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_portfolio_command_source">
<column name="job_name" type="VARCHAR(100)">
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -46,7 +46,8 @@ public class LoanItemListenerStepDefinitions implements En {
private LoanAccountLockRepository accountLockRepository = mock(LoanAccountLockRepository.class);
private TransactionTemplate transactionTemplate = spy(TransactionTemplate.class);
private LoanItemListener loanItemListener = new LoanItemListener(accountLockRepository, transactionTemplate);
private ChunkProcessingLoanItemListener loanItemListener = new ChunkProcessingLoanItemListener(accountLockRepository,
transactionTemplate);
private Exception exception;
@@ -1,80 +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.cob.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class BusinessStepConfigUpdateHandlerTest {
@InjectMocks
private BusinessStepConfigUpdateHandler testObj;
@Mock
private ConfigJobParameterService configJobParameterService;
@Captor
private ArgumentCaptor<String> argumentCaptor;
@Test
void shouldProcessCommandGetsJobName() {
JsonCommand command = mock(JsonCommand.class);
given(command.getUrl()).willReturn("/jobs/jobName/steps");
testObj.processCommand(command);
verify(configJobParameterService).updateStepConfigByJobName(eq(command), argumentCaptor.capture());
assertEquals("jobName", argumentCaptor.getValue());
}
@Test
void shouldProcessCommandGetsJobNameEmptyString() {
JsonCommand command = mock(JsonCommand.class);
given(command.getUrl()).willReturn("/jobs//steps");
testObj.processCommand(command);
verify(configJobParameterService).updateStepConfigByJobName(eq(command), argumentCaptor.capture());
assertEquals("", argumentCaptor.getValue());
}
@Test
void shouldProcessCommandGetsJobNameNull() {
JsonCommand command = mock(JsonCommand.class);
given(command.getUrl()).willReturn("/jobs/" + null + "/steps");
testObj.processCommand(command);
verify(configJobParameterService).updateStepConfigByJobName(eq(command), argumentCaptor.capture());
assertEquals(null, argumentCaptor.getValue());
}
}
@@ -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.cob.service;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.List;
import org.apache.fineract.cob.exceptions.LoanAccountLockCannotBeOverruledException;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.transaction.support.TransactionTemplate;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class InlineLoanCOBExecutorServiceImplTest {
@InjectMocks
private InlineLoanCOBExecutorServiceImpl testObj;
@Mock
private TransactionTemplate transactionTemplate;
@Mock
private InlineLoanCOBExecutionDataParser dataParser;
@Test
void shouldExceptionThrownIfLoanIsAlreadyLocked() {
JsonCommand command = mock(JsonCommand.class);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
when(dataParser.parseExecution(any())).thenReturn(List.of(3L));
when(transactionTemplate.execute(any())).thenThrow(new LoanAccountLockCannotBeOverruledException(""));
assertThrows(LoanAccountLockCannotBeOverruledException.class, () -> testObj.executeInlineJob(command, "INLINE_LOAN_COB"));
}
}
@@ -42,7 +42,7 @@ public class CommandHandlerProviderStepDefinitions implements En {
When("The user processes the command with ID {long}", (Long id) -> {
this.result = commandHandler
.processCommand(JsonCommand.fromExistingCommand(id, null, null, null, null, null, null, null, null, null, null));
.processCommand(JsonCommand.fromExistingCommand(id, null, null, null, null, null, null, null, null, null, null, null));
});
Then("The command ID matches {long}", (Long id) -> {