From 3f1743a3d45f7a18adcc2c80c4ffdbcc569bdc76 Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Fri, 2 Sep 2022 17:42:39 -0500 Subject: [PATCH] Real time Loan Delinquency Classification --- .../api/DelinquencyApiResourceSwagger.java | 3 +- .../LoanDelinquencyTagHistoryRepository.java | 2 + .../DelinquencyWritePlatformService.java | 7 + .../DelinquencyWritePlatformServiceImpl.java | 23 +- .../portfolio/loanaccount/domain/Loan.java | 21 +- .../domain/LoanAccountDomainService.java | 9 + .../domain/LoanAccountDomainServiceJpa.java | 33 +- ...WritePlatformServiceJpaRepositoryImpl.java | 15 + .../DelinquencyBucketsIntegrationTest.java | 355 +++++++++++++++++- .../common/loans/LoanTransactionHelper.java | 26 +- 10 files changed, 451 insertions(+), 43 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java index 9187671ae..36dcc9c44 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java @@ -143,8 +143,7 @@ public final class DelinquencyApiResourceSwagger { public Integer id; @Schema(example = "10") public Long loanId; - @Schema(example = "Delinquent 1") - public String delinquencyRange; + public GetDelinquencyRangesResponse delinquencyRange; @Schema(example = "2013,1,2") public LocalDate addedOnDate; @Schema(example = "2013,2,20") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java index ef57dc10f..472641109 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java @@ -38,4 +38,6 @@ public interface LoanDelinquencyTagHistoryRepository Long countByDelinquencyRange(DelinquencyRange delinquencyRange); + List findByLoan(Loan loan); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java index 77c571c62..b2ee3bac2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.delinquency.service; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; public interface DelinquencyWritePlatformService { @@ -41,4 +42,10 @@ public interface DelinquencyWritePlatformService { void applyDelinquencyTagToLoan(Long loanId, Long ageDays); + void applyDelinquencyTagToLoan(Loan loan, Long ageDays); + + void removeDelinquencyTagToLoan(Loan loan); + + void cleanLoanDelinquencyTags(Loan loan); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java index 644563363..45fa65f63 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java @@ -167,9 +167,26 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat @Override public void applyDelinquencyTagToLoan(Long loanId, Long ageDays) { final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId); - final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket(); - if (delinquencyBucket != null) { - lookUpDelinquencyRange(loan, delinquencyBucket, ageDays); + applyDelinquencyTagToLoan(loan, ageDays); + } + + @Override + public void applyDelinquencyTagToLoan(final Loan loan, Long ageDays) { + if (loan.hasDelinquencyBucket()) { + lookUpDelinquencyRange(loan, loan.getLoanProduct().getDelinquencyBucket(), ageDays); + } + } + + @Override + public void removeDelinquencyTagToLoan(final Loan loan) { + setLoanDelinquencyTag(loan, null); + } + + @Override + public void cleanLoanDelinquencyTags(Loan loan) { + List loanDelinquencyTags = this.loanDelinquencyTagRepository.findByLoan(loan); + if (loanDelinquencyTags != null && loanDelinquencyTags.size() > 0) { + this.loanDelinquencyTagRepository.deleteAll(loanDelinquencyTags); } } 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 3a2c045bd..11171e261 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 @@ -143,8 +143,6 @@ import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.rate.domain.Rate; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks; import org.apache.fineract.useradministration.domain.AppUser; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Entity @@ -153,8 +151,6 @@ import org.springframework.stereotype.Component; @UniqueConstraint(columnNames = { "external_id" }, name = "loan_externalid_UNIQUE") }) public class Loan extends AbstractAuditableWithUTCDateTimeCustom { - private static final Logger LOG = LoggerFactory.getLogger(Loan.class); - /** Disable optimistic locking till batch jobs failures can be fixed **/ @Version int version; @@ -6864,4 +6860,21 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { // Return empty set instead of null to avoid NPE return Optional.ofNullable(this.charges).orElse(new HashSet<>()); } + + public boolean hasDelinquencyBucket() { + return (getLoanProduct().getDelinquencyBucket() != null); + } + + public Long getAgeOfOverdueDays(LocalDate baseDate) { + Long ageOfOverdueDays = 0L; + + List installments = getRepaymentScheduleInstallments(); + for (final LoanRepaymentScheduleInstallment installment : installments) { + if (!installment.isObligationsMet()) { + ageOfOverdueDays = DateUtils.getDifferenceInDays(installment.getDueDate(), baseDate); + break; + } + } + return ageOfOverdueDays; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java index 6dad41349..7d0491d9f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java @@ -62,6 +62,15 @@ public interface LoanAccountDomainService { */ void recalculateAccruals(Loan loan); + /** + * This method is to set a Delinquency Tag If the loan is overdue, If the loan after the repayment transaction is + * not overdue and It has a Delinquency Tag, It is removed + * + * @param loan + * @param transactionDate + */ + void setLoanDelinquencyTag(Loan loan, LocalDate transactionDate); + LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult, LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId, boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 3d50509b9..44a332add 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -20,7 +20,6 @@ package org.apache.fineract.portfolio.loanaccount.domain; import java.math.BigDecimal; import java.time.LocalDate; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -29,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; @@ -60,7 +60,6 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionRecoveryPaymentPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionRecoveryPaymentPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; -import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.holiday.domain.Holiday; import org.apache.fineract.organisation.holiday.domain.HolidayRepository; import org.apache.fineract.organisation.holiday.domain.HolidayStatusType; @@ -80,6 +79,7 @@ import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformService; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; @@ -94,12 +94,12 @@ import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.data.PostDatedChecksStatus; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository; -import org.apache.fineract.useradministration.domain.AppUser; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { @@ -116,12 +116,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final AccountTransferRepository accountTransferRepository; private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; private final LoanAccrualPlatformService loanAccrualPlatformService; - private final PlatformSecurityContext context; private final BusinessEventNotifierService businessEventNotifierService; private final LoanUtilService loanUtilService; private final StandingInstructionRepository standingInstructionRepository; private final PostDatedChecksRepository postDatedChecksRepository; private final LoanCollateralManagementRepository loanCollateralManagementRepository; + private final DelinquencyWritePlatformService delinquencyWritePlatformService; @Transactional @Override @@ -157,7 +157,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment, final String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone, final boolean isLoanToLoanTransfer) { - AppUser currentUser = getAppUserIfPresent(); checkClientOrGroupActive(loan); LoanBusinessEvent repaymentEvent = getLoanRepaymentTypeBusinessEvent(repaymentTransactionType, isRecoveryRepayment, loan); @@ -224,6 +223,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { recalculateAccruals(loan); + setLoanDelinquencyTag(loan, transactionDate); + if (!repaymentTransactionType.isChargeRefund()) { LoanTransactionBusinessEvent transactionRepaymentEvent = getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType, isRecoveryRepayment, newRepaymentTransaction); @@ -536,6 +537,18 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { recalculateAccruals(loan, loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); } + @Override + public void setLoanDelinquencyTag(final Loan loan, final LocalDate transactionDate) { + // Revalidate the Delinquency Classification + final Long ageOfOverdueDays = loan.getAgeOfOverdueDays(transactionDate); + log.debug("Loan {} with {} days and current classification {}", loan.getId(), ageOfOverdueDays); + if (ageOfOverdueDays > 0L) { // If loan is overdue + this.delinquencyWritePlatformService.applyDelinquencyTagToLoan(loan, ageOfOverdueDays); + } else { + this.delinquencyWritePlatformService.removeDelinquencyTagToLoan(loan); + } + } + @Override public void recalculateAccruals(Loan loan, boolean isInterestCalculationHappened) { LocalDate accruedTill = loan.getAccruedTill(); @@ -629,14 +642,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { } } - private AppUser getAppUserIfPresent() { - AppUser user = null; - if (this.context != null) { - user = this.context.getAuthenticatedUserIfPresent(); - } - return user; - } - @Override public CommandProcessingResultBuilder creditBalanceRefund(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount, String noteText, String externalId) { @@ -720,7 +725,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); final ScheduleGeneratorDTO scheduleGeneratorDTO = null; - AppUser appUser = getAppUserIfPresent(); final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() && (loan.getAccruedTill() == null || !foreClosureDate.isEqual(loan.getAccruedTill()))) { @@ -763,7 +767,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { if (payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable).isGreaterThanZero()) { final PaymentDetail paymentDetail = null; String externalId = null; - final LocalDateTime currentDateTime = DateUtils.getLocalDateTimeOfTenant(); payment = LoanTransaction.repayment(loan.getOffice(), payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable), paymentDetail, foreClosureDate, externalId); payment.updateLoan(loan); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index bbb64ae64..46e4bffff 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -522,6 +522,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf updateRecurringCalendarDatesForInterestRecalculation(loan); this.loanAccountDomainService.recalculateAccruals(loan); + this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); // Post Dated Checks if (command.parameterExists("postDatedChecks")) { @@ -784,6 +785,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } updateRecurringCalendarDatesForInterestRecalculation(loan); loanAccountDomainService.recalculateAccruals(loan); + loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan)); } @@ -1148,6 +1150,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); this.loanAccountDomainService.recalculateAccruals(loan); + + this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust); if (newTransactionDetail.isRepaymentType() && newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency())) { eventData.setNewTransactionDetail(newTransactionDetail); @@ -1227,6 +1232,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccountDomainService.recalculateAccruals(loan); + loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction)); return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(waiveInterestTransaction.getId()) .withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) @@ -1244,6 +1251,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); changes.put("locale", command.locale()); changes.put("dateFormat", command.dateFormat()); + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final Loan loan = this.loanAssembler.assembleFrom(loanId); if (command.hasParameter("writeoffReasonId")) { Long writeoffReasonId = command.longValueOfParameterNamed("writeoffReasonId"); @@ -1290,6 +1298,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccountDomainService.recalculateAccruals(loan); + loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff)); return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(writeOff.getId()) .withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) @@ -1310,6 +1319,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); changes.put("locale", command.locale()); changes.put("dateFormat", command.dateFormat()); + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final List existingTransactionIds = new ArrayList<>(); final List existingReversedTransactionIds = new ArrayList<>(); @@ -1346,6 +1356,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } loanAccountDomainService.recalculateAccruals(loan); + loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + businessEventNotifierService.notifyPostBusinessEvent(new LoanCloseBusinessEvent(loan)); // Update loan transaction on repayment. @@ -2731,6 +2743,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf && loan.isFeeCompoundingEnabledForInterestRecalculation()) { this.loanAccountDomainService.recalculateAccruals(loan); } + this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); businessEventNotifierService.notifyPostBusinessEvent(new LoanApplyOverdueChargeBusinessEvent(loan)); } } @@ -2860,6 +2873,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (writeOffTransaction != null) { businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction)); } + this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); return new CommandProcessingResultBuilder() // .withOfficeId(loan.getOfficeId()) // .withClientId(loan.getClientId()) // @@ -3004,6 +3018,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); this.loanAccountDomainService.recalculateAccruals(loan); + this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); return new CommandProcessingResultBuilder() // .withOfficeId(loan.getOfficeId()) // .withClientId(loan.getClientId()) // diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java index 42872d564..242156523 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java @@ -36,12 +36,14 @@ import org.apache.fineract.client.models.DeleteDelinquencyBucketResponse; import org.apache.fineract.client.models.DeleteDelinquencyRangeResponse; 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.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule; 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.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PutDelinquencyBucketResponse; import org.apache.fineract.client.models.PutDelinquencyRangeResponse; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; @@ -63,6 +65,7 @@ public class DelinquencyBucketsIntegrationTest { private ResponseSpecification responseSpec; private RequestSpecification requestSpec; + private static final String principalAmount = "10000"; @BeforeEach public void setup() { @@ -259,27 +262,22 @@ public class DelinquencyBucketsIntegrationTest { final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, delinquencyBucketResponse.getResourceId()); + // Client and Loan account creation final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012"); - final HashMap loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucket.getId()); - final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap)); - - final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, + delinquencyBucket.getId()); + assertNotNull(getLoanProductsProductResponse); log.info("Loan Product Bucket Name: {}", getLoanProductsProductResponse.getDelinquencyBucket().getName()); + assertEquals(getLoanProductsProductResponse.getDelinquencyBucket().getName(), delinquencyBucket.getName()); - final String operationDate = Utils.dateFormatter.format(businessDate); - final String principalAmount = "10000"; + final LocalDate todaysDate = Utils.getLocalDateOfTenant(); + // Older date to have more than one overdue installment + final LocalDate transactionDate = todaysDate.minusDays(65); + String operationDate = Utils.dateFormatter.format(transactionDate); - String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principalAmount).withLoanTermFrequency("12") - .withLoanTermFrequencyAsMonths().withNumberOfRepayments("12").withRepaymentEveryAfter("1") - .withRepaymentFrequencyTypeAsMonths() // - .withInterestRatePerPeriod("2") // - .withExpectedDisbursementDate(operationDate) // - .withInterestTypeAsDecliningBalance() // - .withSubmittedOnDate(operationDate) // - .build(clientId.toString(), loanProductId.toString(), null); - final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); - loanTransactionHelper.approveLoan(operationDate, principalAmount, loanId, null); - loanTransactionHelper.disburseLoanWithNetDisbursalAmount(operationDate, loanId, principalAmount); + // Create Loan Account + final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(), + getLoanProductsProductResponse.getId().toString(), operationDate); // When // Run first time the Job @@ -294,7 +292,8 @@ public class DelinquencyBucketsIntegrationTest { if (getLoanRepaymentSchedule != null) { log.info("Loan with {} periods", getLoanRepaymentSchedule.getPeriods().size()); for (GetLoansLoanIdRepaymentPeriod period : getLoanRepaymentSchedule.getPeriods()) { - log.info("Period number {} for due date {}", period.getPeriod(), period.getDueDate()); + log.info("Period number {} for due date {} and outstanding {}", period.getPeriod(), period.getDueDate(), + period.getTotalOutstandingForPeriod()); } } @@ -321,4 +320,324 @@ public class DelinquencyBucketsIntegrationTest { GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); } + @Test + public void testLoanClassificationRealtime() { + // Given + final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + + 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()); + + // Client and Loan account creation + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012"); + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, + delinquencyBucket.getId()); + assertNotNull(getLoanProductsProductResponse); + log.info("Loan Product Bucket Name: {}", getLoanProductsProductResponse.getDelinquencyBucket().getName()); + assertEquals(getLoanProductsProductResponse.getDelinquencyBucket().getName(), delinquencyBucket.getName()); + + final LocalDate todaysDate = Utils.getLocalDateOfTenant(); + // Older date to have more than one overdue installment + final LocalDate transactionDate = todaysDate.minusDays(45); + String operationDate = Utils.dateFormatter.format(transactionDate); + + // Create Loan Account + final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(), + getLoanProductsProductResponse.getId().toString(), operationDate); + + GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + log.info("Loan Delinquency Range after Disbursement {}", getLoansLoanIdResponse.getDelinquencyRange().getClassification()); + assertNotNull(getLoansLoanIdResponse); + // First Loan Delinquency Classification after Disbursement command + assertEquals(getLoansLoanIdResponse.getDelinquencyRange().getClassification(), classificationExpected); + + printRepaymentSchedule(getLoansLoanIdResponse); + + // Apply a partial repayment + operationDate = Utils.dateFormatter.format(todaysDate); + loanTransactionHelper.makeLoanRepayment(operationDate, 100.0f, loanId); + + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + log.info("Loan Delinquency Range after Repayment {}", getLoansLoanIdResponse.getDelinquencyRange()); + assertNotNull(getLoansLoanIdResponse.getDelinquencyRange()); + // First Loan Delinquency Classification remains after Repayment because the installment is not fully paid + assertEquals(getLoansLoanIdResponse.getDelinquencyRange().getClassification(), classificationExpected); + + // Apply a repayment to get a full paid installment + loanTransactionHelper.makeLoanRepayment(operationDate, 1000.0f, loanId); + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + log.info("Loan Delinquency Range after Repayment {}", getLoansLoanIdResponse.getDelinquencyRange()); + assertNotNull(getLoansLoanIdResponse); + // The Loan Delinquency Classification after Repayment command must be null + assertNull(getLoansLoanIdResponse.getDelinquencyRange()); + // Get the Delinquency Tags + ArrayList getDelinquencyTagsHistory = loanTransactionHelper.getLoanDelinquencyTags(requestSpec, + responseSpec, loanId); + assertNotNull(getDelinquencyTagsHistory); + log.info("Delinquency Tag History items {}", getDelinquencyTagsHistory.size()); + assertEquals(1, getDelinquencyTagsHistory.size()); + assertNotNull(getDelinquencyTagsHistory.get(0).getLiftedOnDate()); + assertEquals(getDelinquencyTagsHistory.get(0).getAddedOnDate(), Utils.getLocalDateOfTenant()); + assertEquals(getDelinquencyTagsHistory.get(0).getLiftedOnDate(), Utils.getLocalDateOfTenant()); + assertEquals(getDelinquencyTagsHistory.get(0).getDelinquencyRange().getClassification(), classificationExpected); + log.info("Delinquency Tag Item with Lifted On {}", getDelinquencyTagsHistory.get(0).getLiftedOnDate()); + } + + @Test + public void testLoanClassificationRealtimeOlderLoan() { + // Given + final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + + ArrayList rangeIds = new ArrayList<>(); + // First Range + String jsonRange = DelinquencyRangesHelper.getAsJSON(4, 30); + PostDelinquencyRangeResponse delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, + jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + GetDelinquencyRangesResponse range = DelinquencyRangesHelper.getDelinquencyRange(requestSpec, responseSpec, + delinquencyRangeResponse.getResourceId()); + final String classificationExpected02 = range.getClassification(); + log.info("Expected Delinquency Range classification after first repayment {}", classificationExpected02); + + // Second Range + jsonRange = DelinquencyRangesHelper.getAsJSON(31, 60); + delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + + range = DelinquencyRangesHelper.getDelinquencyRange(requestSpec, responseSpec, delinquencyRangeResponse.getResourceId()); + final String classificationExpected01 = range.getClassification(); + log.info("Expected Delinquency Range classification after Disbursement {}", classificationExpected01); + + // Third Range + jsonRange = DelinquencyRangesHelper.getAsJSON(61, 90); + delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + + range = DelinquencyRangesHelper.getDelinquencyRange(requestSpec, responseSpec, delinquencyRangeResponse.getResourceId()); + + String jsonBucket = DelinquencyBucketsHelper.getAsJSON(rangeIds); + PostDelinquencyBucketResponse delinquencyBucketResponse = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, + responseSpec, jsonBucket); + assertNotNull(delinquencyBucketResponse); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketResponse.getResourceId()); + + // Client and Loan account creation + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012"); + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, + delinquencyBucket.getId()); + assertNotNull(getLoanProductsProductResponse); + log.info("Loan Product Bucket Name: {}", getLoanProductsProductResponse.getDelinquencyBucket().getName()); + assertEquals(getLoanProductsProductResponse.getDelinquencyBucket().getName(), delinquencyBucket.getName()); + + final LocalDate todaysDate = Utils.getLocalDateOfTenant(); + // Older date to have more than one overdue installment + final LocalDate transactionDate = todaysDate.minusDays(85); + String operationDate = Utils.dateFormatter.format(transactionDate); + + // Create Loan Account + final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(), + getLoanProductsProductResponse.getId().toString(), operationDate); + + GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + assertNotNull(getLoansLoanIdResponse); + log.info("Loan Delinquency Range after Disbursement in null? {}", (getLoansLoanIdResponse.getDelinquencyRange() == null)); + assertNotNull(getLoansLoanIdResponse.getDelinquencyRange()); + log.info("Loan Delinquency Range after Disbursement {}", getLoansLoanIdResponse.getDelinquencyRange()); + // First Loan Delinquency Classification after Disbursement command + assertEquals(getLoansLoanIdResponse.getDelinquencyRange().getClassification(), classificationExpected01); + + printRepaymentSchedule(getLoansLoanIdResponse); + + // Apply a repayment to get a first full paid installment + operationDate = Utils.dateFormatter.format(todaysDate); + PostLoansLoanIdTransactionsResponse loansLoanIdTransactions = loanTransactionHelper.makeLoanRepayment(operationDate, 1050.0f, + loanId); + assertNotNull(loansLoanIdTransactions); + log.info("Loan repayment transaction id {}", loansLoanIdTransactions.getResourceId()); + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + log.info("Loan Delinquency Range after first Repayment {}", getLoansLoanIdResponse.getDelinquencyRange()); + assertNotNull(getLoansLoanIdResponse.getDelinquencyRange()); + // First Loan Delinquency Classification remains after Repayment because the installment is not fully paid + assertEquals(getLoansLoanIdResponse.getDelinquencyRange().getClassification(), classificationExpected02); + + ArrayList getDelinquencyTagsHistory = loanTransactionHelper.getLoanDelinquencyTags(requestSpec, + responseSpec, loanId); + assertNotNull(getDelinquencyTagsHistory); + log.info("Delinquency Tag History items {}", getDelinquencyTagsHistory.size()); + log.info("Delinquency Tag Item with Lifted On {}", getDelinquencyTagsHistory.get(0).getLiftedOnDate()); + assertEquals(getDelinquencyTagsHistory.get(0).getAddedOnDate(), Utils.getLocalDateOfTenant()); + assertEquals(getDelinquencyTagsHistory.get(0).getLiftedOnDate(), Utils.getLocalDateOfTenant()); + assertEquals(getDelinquencyTagsHistory.get(0).getDelinquencyRange().getClassification(), classificationExpected01); + log.info("Loan Id {} with Loan status {}", getLoansLoanIdResponse.getId(), getLoansLoanIdResponse.getStatus().getCode()); + + // Apply a repayment to get a second full paid installment + loansLoanIdTransactions = loanTransactionHelper.makeLoanRepayment(operationDate, 1020.0f, loanId); + assertNotNull(loansLoanIdTransactions); + log.info("Loan repayment transaction id {}", loansLoanIdTransactions.getResourceId()); + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + log.info("Loan Delinquency Range after second Repayment {}", getLoansLoanIdResponse.getDelinquencyRange()); + assertNotNull(getLoansLoanIdResponse); + // The Loan Delinquency Classification after Repayment command must be null + assertNull(getLoansLoanIdResponse.getDelinquencyRange()); + + getDelinquencyTagsHistory = loanTransactionHelper.getLoanDelinquencyTags(requestSpec, responseSpec, loanId); + assertNotNull(getDelinquencyTagsHistory); + log.info("Delinquency Tag History items {}", getDelinquencyTagsHistory.size()); + log.info("Delinquency Tag Item with Lifted On {}", getDelinquencyTagsHistory.get(1).getLiftedOnDate()); + assertEquals(getDelinquencyTagsHistory.get(1).getAddedOnDate(), Utils.getLocalDateOfTenant()); + assertEquals(getDelinquencyTagsHistory.get(1).getLiftedOnDate(), Utils.getLocalDateOfTenant()); + assertEquals(getDelinquencyTagsHistory.get(1).getDelinquencyRange().getClassification(), classificationExpected02); + log.info("Loan Id {} with final Loan status {}", getLoansLoanIdResponse.getId(), getLoansLoanIdResponse.getStatus().getCode()); + } + + @Test + public void testLoanClassificationRealtimeWithReversedRepayment() { + // Given + final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + + ArrayList rangeIds = new ArrayList<>(); + // First Range + String jsonRange = DelinquencyRangesHelper.getAsJSON(4, 30); + PostDelinquencyRangeResponse delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, + jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + GetDelinquencyRangesResponse range = DelinquencyRangesHelper.getDelinquencyRange(requestSpec, responseSpec, + delinquencyRangeResponse.getResourceId()); + final String classificationExpected = range.getClassification(); + log.info("Expected Delinquency Range classification after first repayment {}", classificationExpected); + + String jsonBucket = DelinquencyBucketsHelper.getAsJSON(rangeIds); + PostDelinquencyBucketResponse delinquencyBucketResponse = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, + responseSpec, jsonBucket); + assertNotNull(delinquencyBucketResponse); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketResponse.getResourceId()); + + // Client and Loan account creation + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012"); + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, + delinquencyBucket.getId()); + assertNotNull(getLoanProductsProductResponse); + log.info("Loan Product Bucket Name: {}", getLoanProductsProductResponse.getDelinquencyBucket().getName()); + assertEquals(getLoanProductsProductResponse.getDelinquencyBucket().getName(), delinquencyBucket.getName()); + + final LocalDate todaysDate = Utils.getLocalDateOfTenant(); + // Older date to have more than one overdue installment + final LocalDate transactionDate = todaysDate.minusDays(45); + String operationDate = Utils.dateFormatter.format(transactionDate); + + // Create Loan Account + final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(), + getLoanProductsProductResponse.getId().toString(), operationDate); + + GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + assertNotNull(getLoansLoanIdResponse); + log.info("Loan Delinquency Range after Disbursement in null? {}", (getLoansLoanIdResponse.getDelinquencyRange() == null)); + assertNotNull(getLoansLoanIdResponse.getDelinquencyRange()); + log.info("Loan Delinquency Range after Disbursement {}", getLoansLoanIdResponse.getDelinquencyRange()); + // First Loan Delinquency Classification after Disbursement command + assertEquals(getLoansLoanIdResponse.getDelinquencyRange().getClassification(), classificationExpected); + + printRepaymentSchedule(getLoansLoanIdResponse); + + // Apply a repayment to get a full paid installment + operationDate = Utils.dateFormatter.format(todaysDate); + PostLoansLoanIdTransactionsResponse loansLoanIdTransactions = loanTransactionHelper.makeLoanRepayment(operationDate, 1050.0f, + loanId); + assertNotNull(loansLoanIdTransactions); + log.info("Loan repayment transaction id {}", loansLoanIdTransactions.getResourceId()); + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + log.info("Loan Delinquency Range after Repayment {}", getLoansLoanIdResponse.getDelinquencyRange()); + // Loan Delinquency Classification removed after Repayment because the installment is fully paid + assertNull(getLoansLoanIdResponse.getDelinquencyRange()); + + ArrayList getDelinquencyTagsHistory = loanTransactionHelper.getLoanDelinquencyTags(requestSpec, + responseSpec, loanId); + assertNotNull(getDelinquencyTagsHistory); + log.info("Delinquency Tag History items {}", getDelinquencyTagsHistory.size()); + log.info("Delinquency Tag Item with Lifted On {}", getDelinquencyTagsHistory.get(0).getLiftedOnDate()); + assertEquals(getDelinquencyTagsHistory.get(0).getAddedOnDate(), Utils.getLocalDateOfTenant()); + assertEquals(getDelinquencyTagsHistory.get(0).getLiftedOnDate(), Utils.getLocalDateOfTenant()); + assertEquals(getDelinquencyTagsHistory.get(0).getDelinquencyRange().getClassification(), classificationExpected); + log.info("Loan Id {} with Loan status {}", getLoansLoanIdResponse.getId(), getLoansLoanIdResponse.getStatus().getCode()); + + // Reverse the Previous Loan Repayment + PostLoansLoanIdTransactionsResponse loansLoanIdReverseTransactions = loanTransactionHelper.reverseLoanRepayment(loanId, + loansLoanIdTransactions.getResourceId(), operationDate); + assertNotNull(loansLoanIdReverseTransactions); + log.info("Loan repayment reverse transaction id {}", loansLoanIdReverseTransactions.getResourceId()); + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + log.info("Loan Delinquency Range after Reverse Repayment {}", getLoansLoanIdResponse.getDelinquencyRange()); + // Loan Delinquency Classification goes back after Repayment because the installment is not paid + assertEquals(getLoansLoanIdResponse.getDelinquencyRange().getClassification(), classificationExpected); + + getDelinquencyTagsHistory = loanTransactionHelper.getLoanDelinquencyTags(requestSpec, responseSpec, loanId); + assertNotNull(getDelinquencyTagsHistory); + log.info("Delinquency Tag History items {}", getDelinquencyTagsHistory.size()); + log.info("Delinquency Tag Item with Lifted On {}", getDelinquencyTagsHistory.get(1).getLiftedOnDate()); + assertEquals(getDelinquencyTagsHistory.get(1).getAddedOnDate(), Utils.getLocalDateOfTenant()); + // Second record is open with liftedOn in null + assertNull(getDelinquencyTagsHistory.get(1).getLiftedOnDate()); + assertEquals(getDelinquencyTagsHistory.get(1).getDelinquencyRange().getClassification(), classificationExpected); + log.info("Loan Id {} with final Loan status {}", getLoansLoanIdResponse.getId(), getLoansLoanIdResponse.getStatus().getCode()); + } + + private GetLoanProductsProductIdResponse createLoanProduct(final LoanTransactionHelper loanTransactionHelper, + final Integer delinquencyBucketId) { + final HashMap loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucketId); + final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap)); + return loanTransactionHelper.getLoanProduct(loanProductId); + } + + private Integer createLoanAccount(final LoanTransactionHelper loanTransactionHelper, final String clientId, final String loanProductId, + final String operationDate) { + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principalAmount).withLoanTermFrequency("12") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("12").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths() // + .withInterestRatePerPeriod("2") // + .withExpectedDisbursementDate(operationDate) // + .withInterestTypeAsDecliningBalance() // + .withSubmittedOnDate(operationDate) // + .build(clientId, loanProductId, null); + final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); + loanTransactionHelper.approveLoan(operationDate, principalAmount, loanId, null); + loanTransactionHelper.disburseLoanWithNetDisbursalAmount(operationDate, loanId, principalAmount); + return loanId; + } + + private void printRepaymentSchedule(GetLoansLoanIdResponse getLoansLoanIdResponse) { + GetLoansLoanIdRepaymentSchedule getLoanRepaymentSchedule = getLoansLoanIdResponse.getRepaymentSchedule(); + if (getLoanRepaymentSchedule != null) { + log.info("Loan with {} periods", getLoanRepaymentSchedule.getPeriods().size()); + for (GetLoansLoanIdRepaymentPeriod period : getLoanRepaymentSchedule.getPeriods()) { + log.info("Period number {} for due date {} and outstanding {}", period.getPeriod(), period.getDueDate(), + period.getTotalOutstandingForPeriod()); + } + } + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 15c45722d..ae7379972 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -22,12 +22,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.gson.Gson; +import com.linecorp.armeria.internal.shaded.guava.reflect.TypeToken; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Type; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; @@ -38,8 +40,10 @@ import java.util.Map; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse; import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.util.JSON; import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.Utils; @@ -188,6 +192,14 @@ public class LoanTransactionHelper { return Utils.performServerGet(requestSpec, responseSpec, URL, null); } + public ArrayList getLoanDelinquencyTags(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer loanID) { + final String URL = "/fineract-provider/api/v1/loans/" + loanID + "/delinquencytags?" + Utils.TENANT_IDENTIFIER; + final String response = Utils.performServerGet(requestSpec, responseSpec, URL); + Type delinquencyTagsListType = new TypeToken>() {}.getType(); + return GSON.fromJson(response, delinquencyTagsListType); + } + public Object getLoanProductDetail(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final Integer loanProductId, final String jsonAttributeToGetBack) { final String URL = "/fineract-provider/api/v1/loanproducts/" + loanProductId + "?associations=all&" + Utils.TENANT_IDENTIFIER; @@ -397,6 +409,14 @@ public class LoanTransactionHelper { return (HashMap) performLoanTransaction(createLoanTransactionURL(UNDO, loanId, transactionId), getUndoJsonBody(date), ""); } + public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final String date, final Float amountToBePaid, final Integer loanID) { + return postLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID), getRepaymentBodyAsJSON(date, amountToBePaid)); + } + + public PostLoansLoanIdTransactionsResponse reverseLoanRepayment(final Integer loanId, final Integer transactionId, String date) { + return postLoanTransaction(createLoanTransactionURL(UNDO, loanId, transactionId), getUndoJsonBody(date)); + } + public HashMap makeRepaymentWithPDC(final String date, final Float amountToBePaid, final Integer loanID, final Integer paymentType) { return (HashMap) performLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID), getRepaymentWithPDCBodyAsJSON(date, amountToBePaid, paymentType), ""); @@ -785,7 +805,6 @@ public class LoanTransactionHelper { } private HashMap performLoanTransaction(final String postURLForLoanTransaction, final String jsonToBeSent) { - final HashMap response = Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoanTransaction, jsonToBeSent, "changes"); return (HashMap) response.get("status"); @@ -803,6 +822,11 @@ public class LoanTransactionHelper { return Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoanTransaction, jsonToBeSent, responseAttribute); } + private PostLoansLoanIdTransactionsResponse postLoanTransaction(final String postURLForLoanTransaction, final String jsonToBeSent) { + final String response = Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoanTransaction, jsonToBeSent); + return GSON.fromJson(response, PostLoansLoanIdTransactionsResponse.class); + } + private Object performLoanTransaction(final String postURLForLoanTransaction, final String jsonToBeSent, ResponseSpecification responseValidationError) {