diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java index 678f4858e..f579fa276 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java @@ -57,6 +57,7 @@ public enum JobName { SEND_ASYNCHRONOUS_EVENTS("Send Asynchronous Events"), // PURGE_EXTERNAL_EVENTS("Purge External Events"), // PURGE_PROCESSED_COMMANDS("Purge Processed Commands"), // + ACCRUAL_ACTIVITY_POSTING("Accrual Activity Posting"), // ; private final String name; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccrualActivityRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccrualActivityRepository.java new file mode 100644 index 000000000..d30d4a75c --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccrualActivityRepository.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.portfolio.loanaccount.domain; + +import java.time.LocalDate; +import java.util.Set; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +public interface LoanAccrualActivityRepository extends Repository { + + @Query("select loan.id from Loan loan left join LoanTransaction lt on lt.loan = loan and lt.typeOf = 32 and lt.reversed = false and lt.dateOf = :currentDate inner join LoanRepaymentScheduleInstallment rs on rs.loan = loan and rs.isDownPayment = false and rs.additional = false and rs.dueDate = :currentDate where loan.loanRepaymentScheduleDetail.enableAccrualActivityPosting = true and loan.loanStatus = 300 and lt.id is null ") + Set fetchLoanIdsForAccrualActivityPosting(@Param("currentDate") LocalDate currentDate); + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingService.java new file mode 100644 index 000000000..a7f1dc255 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingService.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.portfolio.loanaccount.service; + +import java.time.LocalDate; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.springframework.transaction.annotation.Transactional; + +public interface LoanAccrualActivityProcessingService { + + @Transactional + void makeAccrualActivityTransaction(Long loanId, LocalDate currentDate); + + Loan makeAccrualActivityTransaction(Loan loan, LocalDate currentDate); + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java index 1958b72cb..fa2e1553b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -54,7 +54,7 @@ public interface LoanWritePlatformService { JsonCommand command, boolean isRecoveryRepayment, String chargeRefundChargeType); @Transactional - Loan makeAccrualActivityTransaction(Loan loan, LoanRepaymentScheduleInstallment installment, LocalDate currentDate); + Loan makeAccrualActivityTransaction(Loan loan, LoanRepaymentScheduleInstallment installment, LocalDate transactionDate); @Transactional CommandProcessingResult makeInterestPaymentWaiver(JsonCommand command); diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AccrualActivityPostingBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AccrualActivityPostingBusinessStep.java index 5e149bb9c..f41922064 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AccrualActivityPostingBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AccrualActivityPostingBusinessStep.java @@ -19,18 +19,11 @@ package org.apache.fineract.cob.loan; import java.time.LocalDate; -import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; -import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; -import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualActivityProcessingService; import org.springframework.stereotype.Component; @Slf4j @@ -38,32 +31,15 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class AccrualActivityPostingBusinessStep implements LoanCOBBusinessStep { - private final BusinessEventNotifierService businessEventNotifierService; - private final LoanAccountDomainService loanAccountDomainService; - private final ExternalIdFactory externalIdFactory; - private final LoanWritePlatformService loanWritePlatformService; + private final LoanAccrualActivityProcessingService loanAccrualActivityProcessingService; @Override public Loan execute(Loan loan) { log.debug("start processing loan accrual activity posting on installment due date with id [{}]", loan.getId()); + final LocalDate currentDate = DateUtils.getBusinessLocalDate(); // check if loan capable for posting - if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) { - final LocalDate currentDate = DateUtils.getBusinessLocalDate(); - // check if loan has installment due on business day - Optional first = loan.getRepaymentScheduleInstallments().stream() - .filter(loanRepaymentScheduleInstallment -> loanRepaymentScheduleInstallment.getDueDate().isEqual(currentDate)) - .findFirst(); - if (first.isPresent()) { - final LoanRepaymentScheduleInstallment installment = first.get(); - // check if there is no not-replayed-accrual-activity related to business date - List loanTransactions = loan.getLoanTransactions(loanTransaction -> loanTransaction.isNotReversed() - && loanTransaction.isAccrualActivity() && loanTransaction.getTransactionDate().isEqual(currentDate)); - if (loanTransactions.isEmpty()) { - loan = loanWritePlatformService.makeAccrualActivityTransaction(loan, installment, currentDate); - } - } - } + loanAccrualActivityProcessingService.makeAccrualActivityTransaction(loan, currentDate); log.debug("end processing loan accrual activity posting on installment due date with id [{}]", loan.getId()); return loan; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/accrualactivityposting/AccrualActivityPostingConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/accrualactivityposting/AccrualActivityPostingConfig.java new file mode 100644 index 000000000..fdb9ae164 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/accrualactivityposting/AccrualActivityPostingConfig.java @@ -0,0 +1,51 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.jobs.accrualactivityposting; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.jobs.service.JobName; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +@Configuration +@RequiredArgsConstructor +public class AccrualActivityPostingConfig { + + private final JobRepository jobRepository; + private final PlatformTransactionManager transactionManager; + + @Bean + protected Step accrualActivityPostingStep(AccrualActivityPostingTasklet accrualActivityPostingTasklet) { + return new StepBuilder(JobName.ACCRUAL_ACTIVITY_POSTING.name(), jobRepository) + .tasklet(accrualActivityPostingTasklet, transactionManager).build(); + } + + @Bean + public Job accrualActivityPostingJob(AccrualActivityPostingTasklet accrualActivityPosting) { + return new JobBuilder(JobName.ACCRUAL_ACTIVITY_POSTING.name(), jobRepository) + .start(accrualActivityPostingStep(accrualActivityPosting)).incrementer(new RunIdIncrementer()).build(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/accrualactivityposting/AccrualActivityPostingTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/accrualactivityposting/AccrualActivityPostingTasklet.java new file mode 100644 index 000000000..58edf038d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/accrualactivityposting/AccrualActivityPostingTasklet.java @@ -0,0 +1,64 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.jobs.accrualactivityposting; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAccrualActivityRepository; +import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualActivityProcessingService; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class AccrualActivityPostingTasklet implements Tasklet { + + private final LoanAccrualActivityProcessingService loanAccrualActivityProcessingService; + private final LoanAccrualActivityRepository loanAccrualActivityRepository; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + final LocalDate yesterday = DateUtils.getBusinessLocalDate().minusDays(1); + List errors = new ArrayList<>(); + Set loanAccounts = loanAccrualActivityRepository.fetchLoanIdsForAccrualActivityPosting(yesterday); + for (Long accountId : loanAccounts) { + try { + loanAccrualActivityProcessingService.makeAccrualActivityTransaction(accountId, yesterday); + } catch (Exception e) { + log.error("Failed to add accrual activity transaction for loan {}", accountId, e); + errors.add(e); + } + } + if (!errors.isEmpty()) { + throw new JobExecutionException(errors); + } + + return RepeatStatus.FINISHED; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java new file mode 100644 index 000000000..51159ba2a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java @@ -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.portfolio.loanaccount.service; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActivityProcessingService { + + private final LoanRepositoryWrapper loanRepositoryWrapper; + private final LoanWritePlatformService loanWritePlatformService; + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void makeAccrualActivityTransaction(Long loanId, final LocalDate currentDate) { + Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); + makeAccrualActivityTransaction(loan, currentDate); + } + + @Override + public Loan makeAccrualActivityTransaction(Loan loan, final LocalDate currentDate) { + if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) { + // check if loan has installment due on business day + Optional first = loan.getRepaymentScheduleInstallments().stream() + .filter(loanRepaymentScheduleInstallment -> loanRepaymentScheduleInstallment.getDueDate().isEqual(currentDate)) + .findFirst(); + if (first.isPresent()) { + final LoanRepaymentScheduleInstallment installment = first.get(); + // check if there is no not-replayed-accrual-activity related to business date + List loanTransactions = loan.getLoanTransactions(loanTransaction -> loanTransaction.isNotReversed() + && loanTransaction.isAccrualActivity() && loanTransaction.getTransactionDate().isEqual(currentDate)); + if (loanTransactions.isEmpty()) { + loan = loanWritePlatformService.makeAccrualActivityTransaction(loan, installment, currentDate); + } + } + } + return loan; + } + +} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index b2272f3b2..a39330bca 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -162,4 +162,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0143_add_accrual_activity_posting_job.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0143_add_accrual_activity_posting_job.xml new file mode 100644 index 000000000..1ba462aa7 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0143_add_accrual_activity_posting_job.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java index 6db3a3d5c..dad8b4578 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java @@ -46,6 +46,7 @@ import org.apache.fineract.client.models.PostLoansLoanIdRequest; import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.integrationtests.common.BusinessStepHelper; import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.SchedulerJobHelper; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.charges.ChargesHelper; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; @@ -76,6 +77,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati private static ChargesHelper chargesHelper; private static InlineLoanCOBHelper inlineLoanCOBHelper; private static BusinessStepHelper businessStepHelper; + private static SchedulerJobHelper schedulerJobHelper; @BeforeAll public static void setup() { @@ -85,6 +87,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati requestSpec.header("Fineract-Platform-TenantId", "default"); responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); + schedulerJobHelper = new SchedulerJobHelper(requestSpec); ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec); chargesHelper = new ChargesHelper(); client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); @@ -167,6 +170,133 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati }); } + // Create Loan with Interest and enabled Accrual Activity Posting + // Approve and disburse loan + // charge penalty with due date as 1st installment + // charge fee with due date as 1st installment + // charge penalty with due date as 3rd installment + // charge fee with due date as 2nd installment + // set business day to the day before closing day of 1st installment, run "Accrual Activity Posting" Job, verify no + // Accrual Activity + // posted + // set business day to the closing day of 1st installment, run "Accrual Activity Posting" Job, verify Accrual + // Activity posted + // set business day to the day after closing day of 1st installment, run "Accrual Activity Posting" Job, verify no + // Accrual Activity + // posted + @Test + public void testAccrualActivityPostingJob() { + final String disbursementDay = "01 January 2023"; + final String repaymentPeriod1DueDate = "01 February 2023"; + final String repaymentPeriod1CloseDate = "02 February 2023"; + final String repaymentPeriod1OneDayBeforeCloseDate = "01 February 2023"; + final String repaymentPeriod1OneDayAfterCloseDate = "03 February 2023"; + final String repaymentPeriod2DueDate = "01 March 2023"; + final String repaymentPeriod3DueDate = "01 April 2023"; + final String creationBusinessDay = "15 May 2023"; + AtomicReference loanId = new AtomicReference<>(); + runAt(creationBusinessDay, () -> { + + Long localLoanProductId = createLoanProductAccountingAccrualPeriodicWithInterest(); + loanId.set(applyForLoanApplicationWithInterest(client.getClientId(), localLoanProductId, BigDecimal.valueOf(40000), + disbursementDay)); + + LOG.info("Test Loan Product Id {1} http://localhost:4200/#/products/loan-products/{}/general", localLoanProductId); + LOG.info("Test Client Id {1} http://localhost:4200/#/clients/{}", client.getClientId()); + LOG.info("Test Loan Id {2} http://localhost:4200/#/clients/{}/loans-accounts/{}/transactions", client.getClientId(), loanId); + + loanTransactionHelper.approveLoan(loanId.get(), new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)) + .dateFormat(DATETIME_PATTERN).approvedOnDate(disbursementDay).locale("en")); + + loanTransactionHelper.disburseLoan(loanId.get(), new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay) + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en")); + + chargePenalty(loanId.get(), 30.0, repaymentPeriod1DueDate); + chargePenalty(loanId.get(), 50.0, repaymentPeriod2DueDate); + chargeFee(loanId.get(), 40.0, repaymentPeriod1DueDate); + chargeFee(loanId.get(), 60.0, repaymentPeriod3DueDate); + + }); + runAt(repaymentPeriod1OneDayBeforeCloseDate, () -> { + schedulerJobHelper.executeAndAwaitJob("Accrual Activity Posting"); + verifyTransactions(loanId.get(), // + transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); + }); + runAt(repaymentPeriod1CloseDate, () -> { + schedulerJobHelper.executeAndAwaitJob("Accrual Activity Posting"); + + verifyTransactions(loanId.get(), // + transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + transaction(90.0, "Accrual Activity", "01 February 2023", 0, 0, 20.0, 40.0, 30.0, 0.0, 0.0)); + }); + runAt(repaymentPeriod1OneDayAfterCloseDate, () -> { + schedulerJobHelper.executeAndAwaitJob("Accrual Activity Posting"); + verifyTransactions(loanId.get(), // + transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + transaction(90.0, "Accrual Activity", "01 February 2023", 0, 0, 20.0, 40.0, 30.0, 0.0, 0.0)); + }); + } + + // Create Loan with Interest and enabled Accrual Activity Posting + // Approve and disburse loan + // charge penalty with due date as 1st installment + // charge fee with due date as 1st installment + // charge penalty with due date as 3rd installment + // charge fee with due date as 2nd installment + // set business day to the day before closing day of 1st installment, run "Accrual Activity Posting" Job, verify no + // Accrual Activity + // posted + // set business day to the closing day of 1st installment, run "Accrual Activity Posting" Job, verify Accrual + // Activity posted + // set business day to the day after closing day of 1st installment, run "Accrual Activity Posting" Job, verify no + // Accrual Activity + // posted + @Test + public void testAccrualActivityPostingJobForMultipleLoans() { + final String disbursementDay = "01 January 2023"; + final String repaymentPeriod1DueDate = "01 February 2023"; + final String repaymentPeriod1CloseDate = "02 February 2023"; + final String creationBusinessDay = "15 May 2023"; + AtomicReference loanId1 = new AtomicReference<>(); + AtomicReference loanId2 = new AtomicReference<>(); + runAt(creationBusinessDay, () -> { + + Long localLoanProductId = createLoanProductAccountingAccrualPeriodicWithInterest(); + + loanId1.set(applyForLoanApplicationWithInterest(client.getClientId(), localLoanProductId, BigDecimal.valueOf(40000), + disbursementDay)); + loanId2.set(applyForLoanApplicationWithInterest(client.getClientId(), localLoanProductId, BigDecimal.valueOf(40000), + disbursementDay)); + + loanTransactionHelper.approveLoan(loanId1.get(), new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)) + .dateFormat(DATETIME_PATTERN).approvedOnDate(disbursementDay).locale("en")); + loanTransactionHelper.approveLoan(loanId2.get(), new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)) + .dateFormat(DATETIME_PATTERN).approvedOnDate(disbursementDay).locale("en")); + + loanTransactionHelper.disburseLoan(loanId1.get(), new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay) + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en")); + loanTransactionHelper.disburseLoan(loanId2.get(), new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay) + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en")); + + chargePenalty(loanId1.get(), 30.0, repaymentPeriod1DueDate); + chargeFee(loanId1.get(), 40.0, repaymentPeriod1DueDate); + + chargePenalty(loanId2.get(), 30.0, repaymentPeriod1DueDate); + chargeFee(loanId2.get(), 40.0, repaymentPeriod1DueDate); + + }); + runAt(repaymentPeriod1CloseDate, () -> { + schedulerJobHelper.executeAndAwaitJob("Accrual Activity Posting"); + + verifyTransactions(loanId1.get(), // + transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + transaction(90.0, "Accrual Activity", "01 February 2023", 0, 0, 20.0, 40.0, 30.0, 0.0, 0.0)); + verifyTransactions(loanId2.get(), // + transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + transaction(90.0, "Accrual Activity", "01 February 2023", 0, 0, 20.0, 40.0, 30.0, 0.0, 0.0)); + }); + } + // Create Loan with Interest and enabled Accrual Activity Posting // Approve and disburse loan // make partial repayment before first installment day