From 220d9fe1e47d328cbe2c6e92f297e04e4e008551 Mon Sep 17 00:00:00 2001 From: taskain7 Date: Tue, 29 Nov 2022 10:35:34 +0100 Subject: [PATCH] [FINERACT-1678] Loan COB Catch Up --- .../fineract/client/util/FineractClient.java | 3 + .../cob/api/LoanCOBCatchUpApiResource.java | 97 +++++++ .../api/LoanCOBCatchUpApiResourceSwagger.java | 53 ++++ .../cob/data/IsCatchUpRunningDTO.java | 31 +++ .../data/LoanIdAndLastClosedBusinessDate.java | 28 ++ .../cob/data/OldestCOBProcessedLoanDTO.java | 31 +++ .../fineract/cob/loan/LoanCOBConstant.java | 2 + ...RetrieveAllNonClosedLoanIdServiceImpl.java | 2 +- .../service/AsyncLoanCOBExecutorService.java | 26 ++ .../AsyncLoanCOBExecutorServiceImpl.java | 107 ++++++++ .../InlineLoanCOBExecutorServiceImpl.java | 48 +++- .../cob/service/LoanCOBCatchUpService.java | 31 +++ .../service/LoanCOBCatchUpServiceImpl.java | 79 ++++++ .../async/CustomAsyncExceptionHandler.java | 37 +++ .../async/SpringAsyncConfig.java | 43 +++ .../jobs/data/JobParametersDTO.java | 32 +++ .../jobs/domain/JobExecutionRepository.java | 14 + .../domain/ScheduledJobDetailRepository.java | 2 + .../loanaccount/domain/LoanRepository.java | 21 +- .../InlineLoanCOBExecutorServiceImplTest.java | 39 ++- .../inlinecob/InlineLoanCOBHelper.java | 54 ++++ .../inlinecob/InlineLoanCOBTest.java | 259 ++++++++++++++++++ 22 files changed, 1028 insertions(+), 11 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResourceSwagger.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/data/IsCatchUpRunningDTO.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/data/OldestCOBProcessedLoanDTO.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/CustomAsyncExceptionHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/SpringAsyncConfig.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobParametersDTO.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBHelper.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBTest.java diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java index 229ec6ab2..dc38f50be 100644 --- a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java +++ b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java @@ -80,6 +80,7 @@ import org.apache.fineract.client.services.InterestRateSlabAKAInterestBandsApi; import org.apache.fineract.client.services.JournalEntriesApi; import org.apache.fineract.client.services.ListReportMailingJobHistoryApi; import org.apache.fineract.client.services.LoanChargesApi; +import org.apache.fineract.client.services.LoanCobCatchUpApi; import org.apache.fineract.client.services.LoanCollateralApi; import org.apache.fineract.client.services.LoanDisbursementDetailsApi; import org.apache.fineract.client.services.LoanProductsApi; @@ -216,6 +217,7 @@ public final class FineractClient { public final JournalEntriesApi journalEntries; public final ListReportMailingJobHistoryApi reportMailings; public final LoanChargesApi loanCharges; + public final LoanCobCatchUpApi loanCobCatchUpApi; public final LoanCollateralApi loanCollaterals; public final LoanProductsApi loanProducts; public final LoanReschedulingApi loanSchedules; @@ -332,6 +334,7 @@ public final class FineractClient { journalEntries = retrofit.create(JournalEntriesApi.class); reportMailings = retrofit.create(ListReportMailingJobHistoryApi.class); loanCharges = retrofit.create(LoanChargesApi.class); + loanCobCatchUpApi = retrofit.create(LoanCobCatchUpApi.class); loanCollaterals = retrofit.create(LoanCollateralApi.class); loanProducts = retrofit.create(LoanProductsApi.class); loanSchedules = retrofit.create(LoanReschedulingApi.class); diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java new file mode 100644 index 000000000..5212c76c7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java @@ -0,0 +1,97 @@ +/** + * 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.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +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.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.cob.data.IsCatchUpRunningDTO; +import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO; +import org.apache.fineract.cob.service.LoanCOBCatchUpService; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.springframework.stereotype.Component; + +@Path("/loans") +@Component +@Tag(name = "Loan COB Catch Up", description = "") +@RequiredArgsConstructor +public class LoanCOBCatchUpApiResource { + + private final DefaultToApiJsonSerializer oldestCOBProcessedLoanSerializeService; + private final DefaultToApiJsonSerializer isCatchUpRunningSerializer; + private final LoanCOBCatchUpService loanCOBCatchUpService; + + @GET + @Path("oldest-cob-closed") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieves the oldest COB processed loan", description = "Retrieves the COB business date and the oldest COB processed loan") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanCOBCatchUpApiResourceSwagger.GetOldestCOBProcessedLoanResponse.class))) }) + public String getOldestCOBProcessedLoan() { + OldestCOBProcessedLoanDTO response = loanCOBCatchUpService.getOldestCOBProcessedLoan(); + + return oldestCOBProcessedLoanSerializeService.serialize(response); + } + + @POST + @Path("catch-up") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Executes Loan COB Catch Up", description = "Executes the Loan COB job on every day from the oldest Loan to the current COB business date") + @ApiResponses({ @ApiResponse(responseCode = "200", description = "All loans are up to date"), + @ApiResponse(responseCode = "202", description = "Catch Up has been started"), + @ApiResponse(responseCode = "400", description = "Catch Up is already running") }) + public Response executeLoanCOBCatchUp() { + if (loanCOBCatchUpService.isCatchUpRunning().isCatchUpRunning()) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + OldestCOBProcessedLoanDTO oldestCOBProcessedLoan = loanCOBCatchUpService.getOldestCOBProcessedLoan(); + if (oldestCOBProcessedLoan.getCobProcessedDate().equals(oldestCOBProcessedLoan.getCobBusinessDate())) { + return Response.status(Response.Status.OK).build(); + } + loanCOBCatchUpService.executeLoanCOBCatchUp(); + + return Response.status(Response.Status.ACCEPTED).build(); + } + + @GET + @Path("is-catch-up-running") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieves whether Loan COB catch up is running", description = "Retrieves whether Loan COB catch up is running, and the current execution date if it is running.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanCOBCatchUpApiResourceSwagger.IsCatchUpRunningResponse.class))) }) + public String isCatchUpRunning() { + IsCatchUpRunningDTO response = loanCOBCatchUpService.isCatchUpRunning(); + + return isCatchUpRunningSerializer.serialize(response); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResourceSwagger.java new file mode 100644 index 000000000..d4706e4dc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResourceSwagger.java @@ -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.api; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import java.util.List; + +final class LoanCOBCatchUpApiResourceSwagger { + + private LoanCOBCatchUpApiResourceSwagger() { + + } + + @Schema(description = "GetOldestCOBProcessedLoanResponse") + public static final class GetOldestCOBProcessedLoanResponse { + + private GetOldestCOBProcessedLoanResponse() {} + + public List loanIds; + @Schema(example = "[2022, 9, 18]") + public LocalDate cobProcessedDate; + @Schema(example = "[2022, 9, 22]") + public LocalDate cobBusinessDate; + } + + @Schema(description = "IsCatchUpRunningResponse") + public static final class IsCatchUpRunningResponse { + + private IsCatchUpRunningResponse() {} + + @Schema(example = "true") + public boolean isCatchUpRunning; + @Schema(example = "[2022, 9, 22]") + public LocalDate processingDate; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/IsCatchUpRunningDTO.java b/fineract-provider/src/main/java/org/apache/fineract/cob/data/IsCatchUpRunningDTO.java new file mode 100644 index 000000000..cef50f56a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/data/IsCatchUpRunningDTO.java @@ -0,0 +1,31 @@ +/** + * 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.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class IsCatchUpRunningDTO { + + private boolean isCatchUpRunning; + private LocalDate processingDate; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java b/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java new file mode 100644 index 000000000..cdebe1185 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java @@ -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.time.LocalDate; + +public interface LoanIdAndLastClosedBusinessDate { + + Long getId(); + + LocalDate getLastClosedBusinessDate(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/OldestCOBProcessedLoanDTO.java b/fineract-provider/src/main/java/org/apache/fineract/cob/data/OldestCOBProcessedLoanDTO.java new file mode 100644 index 000000000..9ec72367c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/data/OldestCOBProcessedLoanDTO.java @@ -0,0 +1,31 @@ +/** + * 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.time.LocalDate; +import java.util.List; +import lombok.Data; + +@Data +public class OldestCOBProcessedLoanDTO { + + private List loanIds; + private LocalDate cobProcessedDate; + private LocalDate cobBusinessDate; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java index f8a142826..782abcef7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java @@ -21,6 +21,7 @@ package org.apache.fineract.cob.loan; public final class LoanCOBConstant { public static final String JOB_NAME = "LOAN_COB"; + public static final String JOB_HUMAN_READABLE_NAME = "Loan COB"; public static final String LOAN_COB_JOB_NAME = "LOAN_CLOSE_OF_BUSINESS"; public static final String LOAN_IDS = "loanIds"; public static final String BUSINESS_STEP_MAP = "businessStepMap"; @@ -29,6 +30,7 @@ public final class LoanCOBConstant { public static final String ALREADY_LOCKED_BY_INLINE_COB_OR_PROCESSED_LOAN_IDS = "alreadyLockedOrProcessedLoanIds"; public static final String INLINE_LOAN_COB_JOB_NAME = "INLINE_LOAN_COB"; public static final String BUSINESS_DATE_PARAMETER_NAME = "BusinessDate"; + public static final String IS_CATCH_UP_PARAMETER_NAME = "IS_CATCH_UP"; public static final String LOAN_COB_PARTITIONER_STEP = "Loan COB partition - Step"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java index a62de399a..58cf35751 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java @@ -30,6 +30,6 @@ public class RetrieveAllNonClosedLoanIdServiceImpl implements RetrieveLoanIdServ @Override public List retrieveLoanIdsNDaysBehind(Long numberOfDays, LocalDate businessDate) { - return loanRepository.findAllNonClosedLoanIdsOneDayBehind(businessDate.minusDays(numberOfDays)); + return loanRepository.findAllNonClosedLoanIdsByLastClosedBusinessDate(businessDate.minusDays(numberOfDays)); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorService.java new file mode 100644 index 000000000..e2001f31f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorService.java @@ -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.service; + +import org.apache.fineract.infrastructure.core.domain.FineractContext; + +public interface AsyncLoanCOBExecutorService { + + void executeLoanCOBCatchUpAsync(FineractContext context); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java new file mode 100644 index 000000000..eef62720e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java @@ -0,0 +1,107 @@ +/** + * 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 java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate; +import org.apache.fineract.cob.loan.LoanCOBConstant; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.domain.FineractContext; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO; +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.ScheduledJobDetailRepository; +import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException; +import org.apache.fineract.infrastructure.jobs.service.JobStarter; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.batch.core.configuration.JobLocator; +import org.springframework.batch.core.launch.NoSuchJobException; +import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; +import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; +import org.springframework.batch.core.repository.JobRestartException; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AsyncLoanCOBExecutorServiceImpl implements AsyncLoanCOBExecutorService { + + private final LoanRepository loanRepository; + private final JobLocator jobLocator; + private final ScheduledJobDetailRepository scheduledJobDetailRepository; + private final JobStarter jobStarter; + private final JobParameterRepository jobParameterRepository; + + @Override + @Async("loanCOBCatchUpThreadPoolTaskExecutor") + public void executeLoanCOBCatchUpAsync(FineractContext context) { + try { + ThreadLocalContextUtil.init(context); + LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE); + List loanIdAndLastClosedBusinessDate = loanRepository + .findOldestCOBProcessedLoan(cobBusinessDate); + LocalDate oldestCOBProcessedDate = !loanIdAndLastClosedBusinessDate.isEmpty() + ? loanIdAndLastClosedBusinessDate.get(0).getLastClosedBusinessDate() + : cobBusinessDate; + if (oldestCOBProcessedDate.isBefore(cobBusinessDate)) { + executeLoanCOBDayByDayUntilCOBBusinessDate(oldestCOBProcessedDate, cobBusinessDate); + } + } catch (NoSuchJobException e) { + throw new JobNotFoundException(LoanCOBConstant.JOB_NAME, e); + } catch (JobInstanceAlreadyCompleteException | JobRestartException | JobParametersInvalidException + | JobExecutionAlreadyRunningException e) { + throw new RuntimeException(e); + } finally { + ThreadLocalContextUtil.reset(); + } + } + + private void executeLoanCOBDayByDayUntilCOBBusinessDate(LocalDate oldestCOBProcessedDate, LocalDate cobBusinessDate) + throws NoSuchJobException, JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, + JobParametersInvalidException, JobRestartException { + Job job = jobLocator.getJob(LoanCOBConstant.JOB_NAME); + ScheduledJobDetail scheduledJobDetail = scheduledJobDetailRepository.findByJobName(LoanCOBConstant.JOB_HUMAN_READABLE_NAME); + LocalDate executingBusinessDate = oldestCOBProcessedDate.plusDays(1); + while (!executingBusinessDate.isAfter(cobBusinessDate)) { + JobParameterDTO jobParameterDTO = new JobParameterDTO(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME, + executingBusinessDate.format(DateTimeFormatter.ISO_DATE)); + Set jobParameters = Collections.singleton(jobParameterDTO); + saveCatchUpJobParameter(scheduledJobDetail); + jobStarter.run(job, scheduledJobDetail, ThreadLocalContextUtil.getContext(), jobParameters); + executingBusinessDate = executingBusinessDate.plusDays(1); + } + } + + private void saveCatchUpJobParameter(ScheduledJobDetail scheduledJobDetail) { + JobParameter jobParameter = new JobParameter(); + jobParameter.setJobId(scheduledJobDetail.getId()); + jobParameter.setParameterName(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME); + jobParameter.setParameterValue("1"); + jobParameterRepository.save(jobParameter); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java index 4ce8f9efb..4ff06aece 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java @@ -24,6 +24,7 @@ import com.google.gson.Gson; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,6 +32,7 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate; import org.apache.fineract.cob.domain.LoanAccountLock; import org.apache.fineract.cob.domain.LoanAccountLockRepository; import org.apache.fineract.cob.domain.LockOwner; @@ -49,6 +51,7 @@ import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException; import org.apache.fineract.infrastructure.jobs.service.InlineExecutorService; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.springbatch.SpringBatchJobConstants; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.jetbrains.annotations.NotNull; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.Job; @@ -73,8 +76,6 @@ import org.springframework.transaction.support.TransactionTemplate; public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService { 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; @@ -83,6 +84,7 @@ public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService loanIds, String jobName) { + LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE); + List loansToBeProcessed = getLoansToBeProcessed(loanIds, cobBusinessDate); + LocalDate executingBusinessDate = getOldestCOBBusinessDate(loansToBeProcessed).plusDays(1); + if (!loansToBeProcessed.isEmpty()) { + while (!executingBusinessDate.isAfter(cobBusinessDate)) { + execute(getLoanIdsToBeProcessed(loansToBeProcessed, executingBusinessDate), jobName, executingBusinessDate); + executingBusinessDate = executingBusinessDate.plusDays(1); + } + } + } + + private List getLoanIdsToBeProcessed(List loansToBeProcessed, LocalDate executingBusinessDate) { + List loanIdsToBeProcessed = new ArrayList<>(); + loansToBeProcessed.forEach(loan -> { + if (loan.getLastClosedBusinessDate() != null) { + if (loan.getLastClosedBusinessDate().isBefore(executingBusinessDate)) { + loanIdsToBeProcessed.add(loan.getId()); + } + } else { + loanIdsToBeProcessed.add(loan.getId()); + } + }); + return loanIdsToBeProcessed; + } + + private void execute(List loanIds, String jobName, LocalDate businessDate) { lockLoanAccounts(loanIds); Job inlineLoanCOBJob; try { @@ -104,9 +132,7 @@ public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService loans) { + LoanIdAndLastClosedBusinessDate oldestLoan = loans.stream().min(Comparator + .comparing(LoanIdAndLastClosedBusinessDate::getLastClosedBusinessDate, Comparator.nullsLast(Comparator.naturalOrder()))) + .orElse(null); + return oldestLoan != null && oldestLoan.getLastClosedBusinessDate() != null ? oldestLoan.getLastClosedBusinessDate() + : ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE).minusDays(1); + } + + private List getLoansToBeProcessed(List loanIds, LocalDate cobBusinessDate) { + return loanRepository.findAllNonClosedLoansBehindByLoanIds(cobBusinessDate, loanIds); + } + private List getLoanAccountLocks(List loanIds) { List loanAccountLocks = new ArrayList<>(); List alreadyLockedLoanIds = new ArrayList<>(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java new file mode 100644 index 000000000..12abcc961 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java @@ -0,0 +1,31 @@ +/** + * 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 org.apache.fineract.cob.data.IsCatchUpRunningDTO; +import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO; + +public interface LoanCOBCatchUpService { + + OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan(); + + void executeLoanCOBCatchUp(); + + IsCatchUpRunningDTO isCatchUpRunning(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java new file mode 100644 index 000000000..30870bb8a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java @@ -0,0 +1,79 @@ +/** + * 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 java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections.CollectionUtils; +import org.apache.fineract.cob.data.IsCatchUpRunningDTO; +import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate; +import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO; +import org.apache.fineract.cob.loan.LoanCOBConstant; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.domain.FineractContext; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.jobs.domain.JobExecutionRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LoanCOBCatchUpServiceImpl implements LoanCOBCatchUpService { + + private final LoanRepository loanRepository; + private final AsyncLoanCOBExecutorService asyncLoanCOBExecutorService; + private final JobExecutionRepository jobExecutionRepository; + private final JobExplorer jobExplorer; + + @Override + public OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan() { + List loanIdAndLastClosedBusinessDate = loanRepository + .findOldestCOBProcessedLoan(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)); + OldestCOBProcessedLoanDTO oldestCOBProcessedLoanDTO = new OldestCOBProcessedLoanDTO(); + oldestCOBProcessedLoanDTO.setLoanIds(loanIdAndLastClosedBusinessDate.stream().map(LoanIdAndLastClosedBusinessDate::getId).toList()); + oldestCOBProcessedLoanDTO.setCobProcessedDate( + loanIdAndLastClosedBusinessDate.stream().map(LoanIdAndLastClosedBusinessDate::getLastClosedBusinessDate).findFirst() + .orElse(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE))); + oldestCOBProcessedLoanDTO.setCobBusinessDate(ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)); + return oldestCOBProcessedLoanDTO; + } + + @Override + public void executeLoanCOBCatchUp() { + FineractContext context = ThreadLocalContextUtil.getContext(); + asyncLoanCOBExecutorService.executeLoanCOBCatchUpAsync(context); + } + + @Override + public IsCatchUpRunningDTO isCatchUpRunning() { + List runningCatchUpExecutionIds = jobExecutionRepository.getRunningJobsByExecutionParameter(LoanCOBConstant.JOB_NAME, + LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME, "1"); + if (CollectionUtils.isNotEmpty(runningCatchUpExecutionIds)) { + JobExecution jobExecution = jobExplorer.getJobExecution(runningCatchUpExecutionIds.get(0)); + String executionDateString = (String) jobExecution.getExecutionContext().get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME); + return new IsCatchUpRunningDTO(true, LocalDate.parse(executionDateString, DateTimeFormatter.ISO_DATE)); + } else { + return new IsCatchUpRunningDTO(false, null); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/CustomAsyncExceptionHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/CustomAsyncExceptionHandler.java new file mode 100644 index 000000000..a96305f76 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/CustomAsyncExceptionHandler.java @@ -0,0 +1,37 @@ +/** + * 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.configuration.async; + +import java.lang.reflect.Method; +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; + +@Slf4j +public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { + + @Override + public void handleUncaughtException(Throwable throwable, Method method, Object... obj) { + + log.error("Exception message - {}", throwable.getMessage()); + log.error("Method name - {}", method.getName()); + for (Object param : obj) { + log.error("Parameter value - {}", param); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/SpringAsyncConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/SpringAsyncConfig.java new file mode 100644 index 000000000..a731937e5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/SpringAsyncConfig.java @@ -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.configuration.async; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +@EnableAsync +public class SpringAsyncConfig implements AsyncConfigurer { + + @Bean(name = "loanCOBCatchUpThreadPoolTaskExecutor") + public ThreadPoolTaskExecutor loanCOBCatchUpThreadPoolTaskExecutor() { + ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); + threadPoolTaskExecutor.setMaxPoolSize(1); + return threadPoolTaskExecutor; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new CustomAsyncExceptionHandler(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobParametersDTO.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobParametersDTO.java new file mode 100644 index 000000000..ca1eff751 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/data/JobParametersDTO.java @@ -0,0 +1,32 @@ +/** + * 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.data; + +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@AllArgsConstructor +@EqualsAndHashCode +public class JobParametersDTO { + + private Set jobParameters; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java index 620961d0d..016a92b0f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java @@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.jobs.domain; import static org.springframework.batch.core.BatchStatus.COMPLETED; import static org.springframework.batch.core.BatchStatus.FAILED; import static org.springframework.batch.core.BatchStatus.STARTED; +import static org.springframework.batch.core.BatchStatus.STARTING; import java.util.List; import java.util.Map; @@ -84,4 +85,17 @@ public class JobExecutionRepository { "UPDATE BATCH_JOB_EXECUTION SET STATUS = :status, START_TIME = null, END_TIME = null WHERE JOB_EXECUTION_ID = :jobExecutionId", Map.of("status", FAILED.name(), "jobExecutionId", stuckJobId)); } + + public List getRunningJobsByExecutionParameter(String jobName, String parameterKeyName, String parameterValue) { + return namedParameterJdbcTemplate.queryForList("SELECT bje.JOB_EXECUTION_ID FROM BATCH_JOB_INSTANCE bji " + + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID " + + "INNER JOIN BATCH_JOB_EXECUTION_PARAMS bjep ON bje.JOB_EXECUTION_ID = bjep.JOB_EXECUTION_ID " + + "WHERE bje.STATUS IN (:statuses) AND bji.JOB_NAME = :jobName AND bjep.KEY_NAME = :parameterKeyName AND bjep.STRING_VAL = :parameterValue AND bje.JOB_INSTANCE_ID NOT IN (" + + "SELECT bje.JOB_INSTANCE_ID FROM BATCH_JOB_INSTANCE bji " + + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID " + + "WHERE bje.STATUS = :completedStatus AND bji.JOB_NAME = :jobName)", + Map.of("statuses", List.of(STARTED.name(), STARTING.name()), "jobName", jobName, "completedStatus", COMPLETED.name(), + "parameterKeyName", parameterKeyName, "parameterValue", parameterValue), + Long.class); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetailRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetailRepository.java index c22cc17db..aec1ea688 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetailRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/ScheduledJobDetailRepository.java @@ -45,4 +45,6 @@ public interface ScheduledJobDetailRepository @Query("select jobDetail from ScheduledJobDetail jobDetail where jobDetail.nodeId = :nodeId or jobDetail.nodeId = 0") List findAllJobs(@Param("nodeId") Integer nodeId); + ScheduledJobDetail findByJobName(String jobName); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java index 9714de9aa..605377861 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.domain; import java.time.LocalDate; import java.util.Collection; import java.util.List; +import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -72,7 +73,7 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat String FIND_ALL_NON_CLOSED = "select loan.id from Loan loan where loan.loanStatus in (100,200,300,303,304)"; - String FIND_ALL_NON_CLOSED_ONE_DAY_BEHIND = "select loan.id from Loan loan where loan.loanStatus in (100,200,300,303,304) and (:last_closed_business_date = loan.lastClosedBusinessDate or loan.lastClosedBusinessDate is NULL)"; + String FIND_ALL_NON_CLOSED_LOANS_BY_LAST_CLOSED_BUSINESS_DATE = "select loan.id from Loan loan where loan.loanStatus in (100,200,300,303,304) and (:businessDate = loan.lastClosedBusinessDate or loan.lastClosedBusinessDate is NULL)"; String FIND_NON_CLOSED_LOAN_THAT_BELONGS_TO_CLIENT = "select loan from Loan loan where loan.id = :loanId and loan.loanStatus = 300 and loan.client.id = :clientId"; @@ -80,6 +81,13 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat String FIND_ID_BY_EXTERNAL_ID = "SELECT loan.id FROM Loan loan WHERE loan.externalId = :externalId"; + // should follow the logic of `FIND_ALL_NON_CLOSED_LOANS_BY_LAST_CLOSED_BUSINESS_DATE` query + String FIND_OLDEST_COB_PROCESSED_LOAN = "select loan.id, loan.lastClosedBusinessDate from Loan loan where loan.lastClosedBusinessDate = (select min(l.lastClosedBusinessDate) from Loan l where loan.loanStatus in (100,200,300,303,304) and l" + + ".lastClosedBusinessDate <> " + ":cobBusinessDate)"; + + String FIND_ALL_NON_CLOSED_LOANS_BEHIND_BY_LOAN_IDS = "select loan.id, loan.lastClosedBusinessDate from Loan loan where loan.id IN :loanIds and loan.loanStatus in (100,200,300,303,304) and (loan.lastClosedBusinessDate <> :cobBusinessDate or " + + "loan.lastClosedBusinessDate is null)"; + @Query(FIND_GROUP_LOANS_DISBURSED_AFTER) List getGroupLoansDisbursedAfter(@Param("disbursementDate") LocalDate disbursementDate, @Param("groupId") Long groupId, @Param("loanType") Integer loanType); @@ -176,7 +184,14 @@ public interface LoanRepository extends JpaRepository, JpaSpecificat @Query(FIND_ID_BY_EXTERNAL_ID) Long findIdByExternalId(@Param("externalId") ExternalId externalId); - @Query(FIND_ALL_NON_CLOSED_ONE_DAY_BEHIND) - List findAllNonClosedLoanIdsOneDayBehind(@Param("last_closed_business_date") LocalDate businessDate); + @Query(FIND_ALL_NON_CLOSED_LOANS_BY_LAST_CLOSED_BUSINESS_DATE) + List findAllNonClosedLoanIdsByLastClosedBusinessDate(@Param("businessDate") LocalDate businessDate); + + @Query(FIND_ALL_NON_CLOSED_LOANS_BEHIND_BY_LOAN_IDS) + List findAllNonClosedLoansBehindByLoanIds(@Param("cobBusinessDate") LocalDate cobBusinessDate, + @Param("loanIds") List loanIds); + + @Query(FIND_OLDEST_COB_PROCESSED_LOAN) + List findOldestCOBProcessedLoan(@Param("cobBusinessDate") LocalDate cobBusinessDate); } diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImplTest.java index 00ee9da1f..d940fd6fb 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImplTest.java @@ -18,16 +18,26 @@ */ package org.apache.fineract.cob.service; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.HashMap; import java.util.List; +import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate; import org.apache.fineract.cob.exceptions.LoanAccountLockCannotBeOverruledException; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; 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.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -45,17 +55,42 @@ class InlineLoanCOBExecutorServiceImplTest { private InlineLoanCOBExecutorServiceImpl testObj; @Mock private TransactionTemplate transactionTemplate; - @Mock private InlineLoanCOBExecutionDataParser dataParser; + @Mock + private LoanRepository loanRepository; @Test void shouldExceptionThrownIfLoanIsAlreadyLocked() { JsonCommand command = mock(JsonCommand.class); + LoanIdAndLastClosedBusinessDate loan = mock(LoanIdAndLastClosedBusinessDate.class); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); + HashMap businessDates = new HashMap<>(); + LocalDate businessDate = LocalDate.now(ZoneId.systemDefault()); + businessDates.put(BusinessDateType.BUSINESS_DATE, businessDate); + businessDates.put(BusinessDateType.COB_DATE, businessDate.minusDays(1)); + ThreadLocalContextUtil.setBusinessDates(businessDates); - when(dataParser.parseExecution(any())).thenReturn(List.of(3L)); when(transactionTemplate.execute(any())).thenThrow(new LoanAccountLockCannotBeOverruledException("")); + when(loanRepository.findAllNonClosedLoansBehindByLoanIds(any(), anyList())).thenReturn(List.of(loan)); assertThrows(LoanAccountLockCannotBeOverruledException.class, () -> testObj.executeInlineJob(command, "INLINE_LOAN_COB")); } + + @Test + void shouldOldestCloseBusinessDateReturnWithCorrectDate() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + LoanIdAndLastClosedBusinessDate loan1 = mock(LoanIdAndLastClosedBusinessDate.class); + LoanIdAndLastClosedBusinessDate loan2 = mock(LoanIdAndLastClosedBusinessDate.class); + LoanIdAndLastClosedBusinessDate loan3 = mock(LoanIdAndLastClosedBusinessDate.class); + when(loan1.getLastClosedBusinessDate()).thenReturn(null); + when(loan2.getLastClosedBusinessDate()).thenReturn(LocalDate.of(2023, 1, 10)); + when(loan3.getLastClosedBusinessDate()).thenReturn(LocalDate.of(2023, 1, 11)); + assertEquals(LocalDate.of(2023, 1, 10), getOldestCOBBusinessDate().invoke(testObj, List.of(loan1, loan2, loan3))); + } + + private Method getOldestCOBBusinessDate() throws NoSuchMethodException { + Method method = InlineLoanCOBExecutorServiceImpl.class.getDeclaredMethod("getOldestCOBBusinessDate", List.class); + method.setAccessible(true); + return method; + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBHelper.java new file mode 100644 index 000000000..000e5bd4e --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBHelper.java @@ -0,0 +1,54 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.inlinecob; + +import com.google.gson.Gson; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.util.HashMap; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.integrationtests.client.IntegrationTest; +import org.apache.fineract.integrationtests.common.Utils; + +@Slf4j +public class InlineLoanCOBHelper extends IntegrationTest { + + private final RequestSpecification requestSpec; + private final ResponseSpecification responseSpec; + + public InlineLoanCOBHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { + this.requestSpec = requestSpec; + this.responseSpec = responseSpec; + } + + public String executeInlineCOB(List loanIds) { + final String EXECUTE_INLINE_COB_API = "/fineract-provider/api/v1/jobs/LOAN_COB/inline"; + log.info("------------------EXECUTE INLINE COB----------------------"); + log.info("------------------Loan IDs: {}----------------------", loanIds); + return Utils.performServerPost(requestSpec, responseSpec, EXECUTE_INLINE_COB_API, buildInlineCOBRequest(loanIds)); + } + + private static String buildInlineCOBRequest(List loanIds) { + final HashMap> map = new HashMap<>(); + map.put("loanIds", loanIds); + log.info("map : {}", map); + return new Gson().toJson(map); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBTest.java new file mode 100644 index 000000000..cf7cb6c40 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/inlinecob/InlineLoanCOBTest.java @@ -0,0 +1,259 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.inlinecob; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.path.json.JsonPath; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.GetDelinquencyBucketsResponse; +import org.apache.fineract.client.models.GetDelinquencyRangesResponse; +import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PostDelinquencyBucketResponse; +import org.apache.fineract.client.models.PostDelinquencyRangeResponse; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.CollateralManagementHelper; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.charges.ChargesHelper; +import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; +import org.apache.fineract.integrationtests.common.products.DelinquencyRangesHelper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@Slf4j +public class InlineLoanCOBTest { + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private InlineLoanCOBHelper inlineLoanCOBHelper; + private LoanTransactionHelper loanTransactionHelper; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + requestSpec.header("Fineract-Platform-TenantId", "default"); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + inlineLoanCOBHelper = new InlineLoanCOBHelper(requestSpec, responseSpec); + } + + @Test + public void testInlineCOB() { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 2)); + GlobalConfigurationHelper.updateValueForGlobalConfiguration(this.requestSpec, this.responseSpec, "10", "0"); + loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec); + Assertions.assertNotNull(clientID); + + Integer overdueFeeChargeId = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentage("1")); + Assertions.assertNotNull(overdueFeeChargeId); + + final Integer loanProductID = createLoanProduct(overdueFeeChargeId.toString()); + Assertions.assertNotNull(loanProductID); + HashMap loanStatusHashMap; + + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + + Assertions.assertNotNull(loanID); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.approveLoan("01 March 2020", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + + String loanDetails = loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + loanStatusHashMap = loanTransactionHelper.disburseLoanWithNetDisbursalAmount("02 March 2020", loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 3, 2)); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanID.longValue())); + GetLoansLoanIdResponse loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID); + Assertions.assertEquals(LocalDate.of(2020, 3, 2), loan.getLastClosedBusinessDate()); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 3, 3)); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanID.longValue())); + + loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID); + Assertions.assertEquals(LocalDate.of(2020, 3, 3), loan.getLastClosedBusinessDate()); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 3, 10)); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanID.longValue())); + + loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID); + Assertions.assertEquals(LocalDate.of(2020, 3, 10), loan.getLastClosedBusinessDate()); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + + @Test + public void testInlineCOBCatchUpLoans() { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec); + Assertions.assertNotNull(clientID); + + Integer overdueFeeChargeId = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentage("1")); + Assertions.assertNotNull(overdueFeeChargeId); + + ArrayList rangeIds = new ArrayList<>(); + // First Range + String jsonRange = DelinquencyRangesHelper.getAsJSON(1, 3); + PostDelinquencyRangeResponse delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, + jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + jsonRange = DelinquencyRangesHelper.getAsJSON(4, 60); + + GetDelinquencyRangesResponse range = DelinquencyRangesHelper.getDelinquencyRange(requestSpec, responseSpec, + delinquencyRangeResponse.getResourceId()); + + // Second Range + delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + + range = DelinquencyRangesHelper.getDelinquencyRange(requestSpec, responseSpec, delinquencyRangeResponse.getResourceId()); + final String classificationExpected = range.getClassification(); + log.info("Expected Delinquency Range classification after Disbursement {}", classificationExpected); + + String jsonBucket = DelinquencyBucketsHelper.getAsJSON(rangeIds); + PostDelinquencyBucketResponse delinquencyBucketResponse = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, + responseSpec, jsonBucket); + assertNotNull(delinquencyBucketResponse); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketResponse.getResourceId()); + + final Integer loanProductID = createLoanProduct(loanTransactionHelper, delinquencyBucket.getId()); + + Assertions.assertNotNull(loanProductID); + HashMap loanStatusHashMap; + + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020"); + + Assertions.assertNotNull(loanID); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.approveLoan("01 March 2020", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + + String loanDetails = loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + loanStatusHashMap = loanTransactionHelper.disburseLoanWithNetDisbursalAmount("02 March 2020", loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 3, 2)); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanID.longValue())); + GetLoansLoanIdResponse loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID); + ArrayList loanDelinquencyTags = loanTransactionHelper.getLoanDelinquencyTags(requestSpec, + responseSpec, loanID); + Assertions.assertTrue(loanDelinquencyTags.isEmpty()); + Assertions.assertEquals(LocalDate.of(2020, 3, 2), loan.getLastClosedBusinessDate()); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 4, 5)); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanID.longValue())); + + loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID); + loanDelinquencyTags = loanTransactionHelper.getLoanDelinquencyTags(requestSpec, responseSpec, loanID); + Assertions.assertEquals(LocalDate.of(2020, 4, 5), loan.getLastClosedBusinessDate()); + Assertions.assertEquals(1, loanDelinquencyTags.size()); + Assertions.assertEquals(LocalDate.of(2020, 4, 3), loanDelinquencyTags.get(0).getAddedOnDate()); + + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 4, 10)); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanID.longValue())); + + loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID); + loanDelinquencyTags = loanTransactionHelper.getLoanDelinquencyTags(requestSpec, responseSpec, loanID); + Assertions.assertEquals(LocalDate.of(2020, 4, 10), loan.getLastClosedBusinessDate()); + Assertions.assertEquals(2, loanDelinquencyTags.size()); + Assertions.assertEquals(LocalDate.of(2020, 4, 3), loanDelinquencyTags.get(1).getAddedOnDate()); + Assertions.assertEquals(LocalDate.of(2020, 4, 6), loanDelinquencyTags.get(0).getAddedOnDate()); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + + private Integer createLoanProduct(final String chargeId) { + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4") + .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1") + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance() + .build(chargeId); + return this.loanTransactionHelper.getLoanProductId(loanProductJSON); + } + + private Integer createLoanProduct(final LoanTransactionHelper loanTransactionHelper, final Integer delinquencyBucketId) { + final HashMap loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucketId); + return loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap)); + } + + private Integer applyForLoanApplication(final String clientID, final String loanProductID, final String savingsID, final String date) { + + List collaterals = new ArrayList<>(); + final Integer collateralId = CollateralManagementHelper.createCollateralProduct(this.requestSpec, this.responseSpec); + Assertions.assertNotNull(collateralId); + final Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(this.requestSpec, this.responseSpec, clientID, + collateralId); + Assertions.assertNotNull(clientCollateralId); + addCollaterals(collaterals, clientCollateralId, BigDecimal.valueOf(1)); + + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("15,000.00").withLoanTermFrequency("4") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("4").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("2").withAmortizationTypeAsEqualInstallments() + .withInterestTypeAsDecliningBalance().withInterestCalculationPeriodTypeSameAsRepaymentPeriod() + .withExpectedDisbursementDate(date).withSubmittedOnDate(date).withCollaterals(collaterals) + .build(clientID, loanProductID, savingsID); + return this.loanTransactionHelper.getLoanId(loanApplicationJSON); + } + + private void addCollaterals(List collaterals, Integer collateralId, BigDecimal quantity) { + collaterals.add(collaterals(collateralId, quantity)); + } + + private HashMap collaterals(Integer collateralId, BigDecimal quantity) { + HashMap collateral = new HashMap<>(2); + collateral.put("clientCollateralId", collateralId.toString()); + collateral.put("quantity", quantity.toString()); + return collateral; + } +}