FINERACT-2104: Accrual Activity Posting Job

This commit is contained in:
Soma Sörös
2024-07-18 12:57:38 +02:00
committed by Adam Saghy
parent 38d994452d
commit 905a733f9e
11 changed files with 430 additions and 29 deletions
@@ -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;
@@ -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<Loan, Long> {
@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<Long> fetchLoanIdsForAccrualActivityPosting(@Param("currentDate") LocalDate currentDate);
}
@@ -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);
}
@@ -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);
@@ -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<LoanRepaymentScheduleInstallment> 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<LoanTransaction> 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;
@@ -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();
}
}
@@ -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<Throwable> errors = new ArrayList<>();
Set<Long> 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;
}
}
@@ -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<LoanRepaymentScheduleInstallment> 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<LoanTransaction> loanTransactions = loan.getLoanTransactions(loanTransaction -> loanTransaction.isNotReversed()
&& loanTransaction.isAccrualActivity() && loanTransaction.getTransactionDate().isEqual(currentDate));
if (loanTransactions.isEmpty()) {
loan = loanWritePlatformService.makeAccrualActivityTransaction(loan, installment, currentDate);
}
}
}
return loan;
}
}
@@ -162,4 +162,5 @@
<include file="parts/0140_trial_balance_with_asset_transfer_update.xml" relativeToChangelogFile="true" />
<include file="parts/0141_add_interest_payment_waiver_transaction_type.xml" relativeToChangelogFile="true" />
<include file="parts/0142_add_accrual_activity_transaction.xml" relativeToChangelogFile="true" />
<include file="parts/0143_add_accrual_activity_posting_job.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<insert tableName="job">
<column name="name" value="Accrual Activity Posting"/>
<column name="display_name" value="Accrual Activity Posting"/>
<column name="cron_expression" value="0 0 1 * * ?"/>
<column name="create_time" valueDate="${current_datetime}"/>
<column name="task_priority" valueNumeric="5"/>
<column name="group_name"/>
<column name="previous_run_start_time"/>
<column name="job_key" value="Accrual Activity Posting1 _ DEFAULT"/>
<column name="initializing_errorlog"/>
<column name="is_active" valueBoolean="false"/>
<column name="currently_running" valueBoolean="false"/>
<column name="updates_allowed" valueBoolean="true"/>
<column name="scheduler_group" valueNumeric="0"/>
<column name="is_misfired" valueBoolean="false"/>
<column name="node_id" valueNumeric="1"/>
<column name="is_mismatched_job" valueBoolean="true"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -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<Long> 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<Long> loanId1 = new AtomicReference<>();
AtomicReference<Long> 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