diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResourceSwagger.java index 56a2c4518..0eb522a2b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/glaccount/api/GLAccountsApiResourceSwagger.java @@ -156,6 +156,30 @@ final class GLAccountsApiResourceSwagger { @Schema(example = "Cash at Bangalore") public String name; + + @Schema(example = "100001") + public String glCode; + + @Schema(example = "true") + public Boolean manualEntriesAllowed; + + @Schema(example = "1") + public Integer type; + + @Schema(example = "10") + public Long tagId; + + @Schema(example = "1") + public Long parentId; + + @Schema(example = "1") + public Integer usage; + + @Schema(example = "Desc") + public String description; + + @Schema(example = "false") + public Boolean disabled; } @Schema(description = "PutGLAccountsResponse") diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/common/InitialisationTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/common/InitialisationTasklet.java index 3fff306df..7e7b3c0c6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/common/InitialisationTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/common/InitialisationTasklet.java @@ -59,6 +59,8 @@ public class InitialisationTasklet implements Tasklet { businessDates.put(BusinessDateType.COB_DATE, businessDate); businessDates.put(BusinessDateType.BUSINESS_DATE, businessDate.plusDays(1)); ThreadLocalContextUtil.setBusinessDates(businessDates); + log.debug("Initialisation with Business Date [{}], COB Date [{}] and Action Context [{}]", businessDate.plusDays(1), businessDate, + ThreadLocalContextUtil.getActionContext()); return RepeatStatus.FINISHED; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStep.java index 8d727e1ff..5b7906f8e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/AddPeriodicAccrualEntriesBusinessStep.java @@ -19,6 +19,7 @@ package org.apache.fineract.cob.loan; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.cob.exceptions.BusinessStepException; import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -28,17 +29,20 @@ import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor +@Slf4j public class AddPeriodicAccrualEntriesBusinessStep implements LoanCOBBusinessStep { private final LoanAccrualPlatformService loanAccrualPlatformService; @Override public Loan execute(Loan loan) { + log.debug("start processing period accrual business step for loan with Id [{}]", loan.getId()); try { loanAccrualPlatformService.addPeriodicAccruals(DateUtils.getBusinessLocalDate(), loan); } catch (MultiException e) { throw new BusinessStepException(String.format("Fail to process period accrual for loan id [%s]", loan.getId()), e); } + log.debug("end processing period accrual business step for loan Id [{}]", loan.getId()); return loan; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStep.java index 7ab04af41..474142ab5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentDueBusinessStep.java @@ -21,6 +21,7 @@ package org.apache.fineract.cob.loan; import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.event.business.domain.loan.repayment.LoanRepaymentDueBusinessEvent; @@ -29,6 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.springframework.stereotype.Component; +@Slf4j @Component @RequiredArgsConstructor public class CheckLoanRepaymentDueBusinessStep implements LoanCOBBusinessStep { @@ -38,6 +40,7 @@ public class CheckLoanRepaymentDueBusinessStep implements LoanCOBBusinessStep { @Override public Loan execute(Loan loan) { + log.debug("start processing loan repayment due business step loan for loan with id [{}]", loan.getId()); Long numberOfDaysBeforeDueDateToRaiseEvent = configurationDomainService.retrieveRepaymentDueDays(); final LocalDate currentDate = DateUtils.getBusinessLocalDate(); final List loanRepaymentScheduleInstallments = loan.getRepaymentScheduleInstallments(); @@ -48,6 +51,7 @@ public class CheckLoanRepaymentDueBusinessStep implements LoanCOBBusinessStep { break; } } + log.debug("end processing loan repayment due business step loan for loan with id [{}]", loan.getId()); return loan; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java index 531720bfb..c370fef36 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckLoanRepaymentOverdueBusinessStep.java @@ -21,6 +21,7 @@ package org.apache.fineract.cob.loan; import java.time.LocalDate; import java.util.List; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.event.business.domain.loan.repayment.LoanRepaymentOverdueBusinessEvent; @@ -29,6 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.springframework.stereotype.Component; +@Slf4j @Component @RequiredArgsConstructor public class CheckLoanRepaymentOverdueBusinessStep implements LoanCOBBusinessStep { @@ -38,6 +40,7 @@ public class CheckLoanRepaymentOverdueBusinessStep implements LoanCOBBusinessSte @Override public Loan execute(Loan loan) { + log.debug("start processing loan repayment overdue business step for loan with Id [{}]", loan.getId()); Long numberOfDaysAfterDueDateToRaiseEvent = configurationDomainService.retrieveRepaymentOverdueDays(); final LocalDate currentDate = DateUtils.getBusinessLocalDate(); final List loanRepaymentScheduleInstallments = loan.getRepaymentScheduleInstallments(); @@ -50,6 +53,7 @@ public class CheckLoanRepaymentOverdueBusinessStep implements LoanCOBBusinessSte } } } + log.debug("end processing loan repayment overdue business step for loan with Id [{}]", loan.getId()); return loan; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/SetLoanDelinquencyTagsBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/SetLoanDelinquencyTagsBusinessStep.java index d0a1168e8..3c8c8a2bf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/SetLoanDelinquencyTagsBusinessStep.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/SetLoanDelinquencyTagsBusinessStep.java @@ -18,9 +18,15 @@ */ package org.apache.fineract.cob.loan; +import static org.apache.fineract.infrastructure.core.service.MeasuringUtil.measure; + +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.domain.ActionContext; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.springframework.stereotype.Component; @@ -34,8 +40,37 @@ public class SetLoanDelinquencyTagsBusinessStep implements LoanCOBBusinessStep { @Override public Loan execute(Loan loan) { - log.debug("Set Loan Delinquency Tags Business Step {}", loan.getId()); - loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + if (loan == null) { + log.debug("Ignoring delinquency tag processing for null loan."); + return null; + } + + String externalId = Optional.ofNullable(loan.getExternalId()).map(ExternalId::getValue).orElse(null); + measure(() -> { + try { + log.debug("Starting delinquency tag processing for loan with Id [{}], account number [{}], external Id [{}]", loan.getId(), + loan.getAccountNumber(), externalId); + + // Change the Action Context to DEFAULT for Business Date so that we can compare the loan due date to + // the + // current date and not the previous (COB) date. + ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); + loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + } catch (RuntimeException re) { + log.error( + "Received [{}] exception while processing delinquency tag for loan with Id [{}], account number [{}], external Id [{}]", + re.getMessage(), loan.getId(), loan.getAccountNumber(), externalId, re); + + throw re; + } finally { + // Change the Action Context back to COB to resume COB steps. + ThreadLocalContextUtil.setActionContext(ActionContext.COB); + } + }, duration -> { + log.debug("Ending delinquency tag processing for loan with Id [{}], account number [{}], external Id [{}], finished in [{}]ms", + loan.getId(), loan.getAccountNumber(), externalId, duration.toMillis()); + }); + return loan; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/MeasuringUtil.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/MeasuringUtil.java index 29f7f4c6a..6979e6ece 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/MeasuringUtil.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/MeasuringUtil.java @@ -42,9 +42,13 @@ public final class MeasuringUtil { public static T measure(Supplier s, BiConsumer c) { StopWatch sw = new StopWatch(); sw.start(); - T result = s.get(); - sw.stop(); - c.accept(result, Duration.ofMillis(sw.getTotalTimeMillis())); + T result = null; + try { + result = s.get(); + } finally { + sw.stop(); + c.accept(result, Duration.ofMillis(sw.getTotalTimeMillis())); + } return result; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index eca63486e..47b8940f6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -3115,7 +3115,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransaction, LoanEvent event) { if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isMerchantIssuedRefund() - || repaymentTransaction.isPayoutRefund() || repaymentTransaction.isChargeRefund()) { + || repaymentTransaction.isPayoutRefund() || repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment()) { if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) { final List dataValidationErrors = new ArrayList<>(); diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/SetLoanDelinquencyTagsBusinessStepTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/SetLoanDelinquencyTagsBusinessStepTest.java new file mode 100644 index 000000000..584dde251 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/SetLoanDelinquencyTagsBusinessStepTest.java @@ -0,0 +1,202 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.cob.loan; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import java.lang.reflect.Constructor; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.RandomUtils; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Test class for {@link SetLoanDelinquencyTagsBusinessStep} + */ +@ExtendWith(MockitoExtension.class) +public class SetLoanDelinquencyTagsBusinessStepTest { + + /** + * The mock {@link LoanAccountDomainService} class. + */ + @Mock + private LoanAccountDomainService loanAccountDomainService; + + /** + * The class under test. + */ + private SetLoanDelinquencyTagsBusinessStep underTest; + + /** + * The loan delinquency classification step enum name. + */ + private static final String BUSINESS_STEP_ENUM_NAME = "LOAN_DELINQUENCY_CLASSIFICATION"; + + /** + * The loan delinquency classification step readable name. + */ + private static final String BUSINESS_STEP_READABLE_NAME = "Loan Delinquency Classification"; + + /** + * Setup context before each test. + */ + @BeforeEach + public void setUp() { + ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); + ThreadLocalContextUtil + .setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault())))); + underTest = new SetLoanDelinquencyTagsBusinessStep(loanAccountDomainService); + } + + /** + * Tests {@link SetLoanDelinquencyTagsBusinessStep#execute(Loan)} success scenario. + * + * @throws Exception + * for any failures. + */ + @Test + public void testExecuteSuccessScenario() throws Exception { + // given + doNothing().when(loanAccountDomainService).setLoanDelinquencyTag(any(Loan.class), any(LocalDate.class)); + Loan loanForProcessing = createLoan(); + + // when + Loan processedLoan = underTest.execute(loanForProcessing); + + // then + verify(loanAccountDomainService).setLoanDelinquencyTag(any(Loan.class), any(LocalDate.class)); + assertEquals(processedLoan, loanForProcessing); + } + + /** + * Tests {@link SetLoanDelinquencyTagsBusinessStep#execute(Loan)} when loan is null. + * + * @throws Exception + * for any failures. + */ + @Test + public void testNullLoanScenario() { + // given + Loan loanForProcessing = null; + + // when + Loan processedLoan = underTest.execute(loanForProcessing); + + // then + verifyNoInteractions(loanAccountDomainService); + assertNull(processedLoan); + } + + /** + * Tests {@link SetLoanDelinquencyTagsBusinessStep#execute(Loan)} when exception is thrown. + * + * @throws Exception + * for any failures. + */ + @Test + public void testExecuteWhenSetLoanDelinquencyTagFails() throws Exception { + // given + doThrow(new RuntimeException()).when(loanAccountDomainService).setLoanDelinquencyTag(any(Loan.class), any(LocalDate.class)); + Loan loanForProcessing = createLoan(); + + // when + final Throwable thrownException = assertThrows(RuntimeException.class, () -> underTest.execute(loanForProcessing)); + + // then + verify(loanAccountDomainService).setLoanDelinquencyTag(any(Loan.class), any(LocalDate.class)); + assertTrue(thrownException.getClass().isAssignableFrom(RuntimeException.class)); + } + + /** + * Tests {@link SetLoanDelinquencyTagsBusinessStep#getEnumStyledName()} + */ + @Test + public void testGetEnumStyledNameSuccessScenario() { + final String actualEnumName = underTest.getEnumStyledName(); + + assertNotNull(actualEnumName); + assertEquals(BUSINESS_STEP_ENUM_NAME, actualEnumName); + } + + /** + * Tests {@link SetLoanDelinquencyTagsBusinessStep#getHumanReadableName()} + */ + @Test + public void testGetHumanReadableNameSuccessScenario() { + final String actualEnumName = underTest.getHumanReadableName(); + + assertNotNull(actualEnumName); + assertEquals(BUSINESS_STEP_READABLE_NAME, actualEnumName); + } + + /** + * creates a new {@link Loan} with random values. + * + * @return the new {@link Loan} instance + * @throws Exception + * for any failures + */ + private Loan createLoan() throws Exception { + Map loanDataMap = new HashMap<>(); + loanDataMap.put("id", RandomUtils.nextLong(1, 1000)); + loanDataMap.put("externalId", ExternalId.generate()); + loanDataMap.put("accountNumber", RandomStringUtils.randomNumeric(10)); + return setupLoanData(loanDataMap); + } + + /** + * Sets up and returns the {@link Loan} with provided product details. + * + * @param loanDataMap + * map with loan key value + * @return the {@link Loan} instance + * @throws Exception + * for any failures + */ + protected Loan setupLoanData(final Map loanDataMap) throws Exception { + final Constructor constructor = Loan.class.getDeclaredConstructor(); + constructor.setAccessible(true); + final Loan loan = constructor.newInstance(); + loanDataMap.forEach((key, value) -> ReflectionTestUtils.setField(loan, key, value)); + return loan; + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java index 9408b9563..df7637ebe 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java @@ -374,18 +374,9 @@ public class ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest { Integer resourceId = (Integer) this.loanTransactionHelper.makeRepaymentTypePayment(REPAYMENT, "06 January 2022", 13000.00f, this.disbursedLoanID, "resourceId"); Assertions.assertNotNull(resourceId); - - if (repaymentTransactionType.equalsIgnoreCase(REPAYMENT)) { - ArrayList errors = (ArrayList) this.loanTransactionHelperValidationError.makeRepaymentTypePayment( - repaymentTransactionType, "06 January 2022", 1.00f, this.disbursedLoanID, CommonConstants.RESPONSE_ERROR); - - assertEquals("error.msg.loan.repayment.or.waiver.account.is.not.active", - errors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE)); - } else { - resourceId = (Integer) this.loanTransactionHelper.makeRepaymentTypePayment(repaymentTransactionType, "06 January 2022", 1.00f, - this.disbursedLoanID, "resourceId"); - Assertions.assertNotNull(resourceId); - } + resourceId = (Integer) this.loanTransactionHelper.makeRepaymentTypePayment(repaymentTransactionType, "06 January 2022", 1.00f, + this.disbursedLoanID, "resourceId"); + Assertions.assertNotNull(resourceId); } @Test 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 index f06b744d5..fc003432c 100644 --- 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 @@ -207,12 +207,12 @@ public class InlineLoanCOBTest { 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)); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 4, 4)); 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(LocalDate.of(2020, 4, 4), loan.getLastClosedBusinessDate()); Assertions.assertEquals(1, loanDelinquencyTags.size()); Assertions.assertEquals(LocalDate.of(2020, 4, 3), loanDelinquencyTags.get(0).getAddedOnDate());