Real time Loan Delinquency Classification

This commit is contained in:
Jose Alberto Hernandez
2022-09-06 12:13:45 +02:00
committed by Arnold Gálovics
parent 17b36a0919
commit 3f1743a3d4
10 changed files with 451 additions and 43 deletions
@@ -143,8 +143,7 @@ public final class DelinquencyApiResourceSwagger {
public Integer id; public Integer id;
@Schema(example = "10") @Schema(example = "10")
public Long loanId; public Long loanId;
@Schema(example = "Delinquent 1") public GetDelinquencyRangesResponse delinquencyRange;
public String delinquencyRange;
@Schema(example = "2013,1,2") @Schema(example = "2013,1,2")
public LocalDate addedOnDate; public LocalDate addedOnDate;
@Schema(example = "2013,2,20") @Schema(example = "2013,2,20")
@@ -38,4 +38,6 @@ public interface LoanDelinquencyTagHistoryRepository
Long countByDelinquencyRange(DelinquencyRange delinquencyRange); Long countByDelinquencyRange(DelinquencyRange delinquencyRange);
List<LoanDelinquencyTagHistory> findByLoan(Loan loan);
} }
@@ -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.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
public interface DelinquencyWritePlatformService { public interface DelinquencyWritePlatformService {
@@ -41,4 +42,10 @@ public interface DelinquencyWritePlatformService {
void applyDelinquencyTagToLoan(Long loanId, Long ageDays); void applyDelinquencyTagToLoan(Long loanId, Long ageDays);
void applyDelinquencyTagToLoan(Loan loan, Long ageDays);
void removeDelinquencyTagToLoan(Loan loan);
void cleanLoanDelinquencyTags(Loan loan);
} }
@@ -167,9 +167,26 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
@Override @Override
public void applyDelinquencyTagToLoan(Long loanId, Long ageDays) { public void applyDelinquencyTagToLoan(Long loanId, Long ageDays) {
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId); final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket(); applyDelinquencyTagToLoan(loan, ageDays);
if (delinquencyBucket != null) { }
lookUpDelinquencyRange(loan, delinquencyBucket, 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<LoanDelinquencyTagHistory> loanDelinquencyTags = this.loanDelinquencyTagRepository.findByLoan(loan);
if (loanDelinquencyTags != null && loanDelinquencyTags.size() > 0) {
this.loanDelinquencyTagRepository.deleteAll(loanDelinquencyTags);
} }
} }
@@ -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.rate.domain.Rate;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Entity @Entity
@@ -153,8 +151,6 @@ import org.springframework.stereotype.Component;
@UniqueConstraint(columnNames = { "external_id" }, name = "loan_externalid_UNIQUE") }) @UniqueConstraint(columnNames = { "external_id" }, name = "loan_externalid_UNIQUE") })
public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private static final Logger LOG = LoggerFactory.getLogger(Loan.class);
/** Disable optimistic locking till batch jobs failures can be fixed **/ /** Disable optimistic locking till batch jobs failures can be fixed **/
@Version @Version
int version; int version;
@@ -6864,4 +6860,21 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
// Return empty set instead of null to avoid NPE // Return empty set instead of null to avoid NPE
return Optional.ofNullable(this.charges).orElse(new HashSet<>()); return Optional.ofNullable(this.charges).orElse(new HashSet<>());
} }
public boolean hasDelinquencyBucket() {
return (getLoanProduct().getDelinquencyBucket() != null);
}
public Long getAgeOfOverdueDays(LocalDate baseDate) {
Long ageOfOverdueDays = 0L;
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment installment : installments) {
if (!installment.isObligationsMet()) {
ageOfOverdueDays = DateUtils.getDifferenceInDays(installment.getDueDate(), baseDate);
break;
}
}
return ageOfOverdueDays;
}
} }
@@ -62,6 +62,15 @@ public interface LoanAccountDomainService {
*/ */
void recalculateAccruals(Loan loan); 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, LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult,
LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId, LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId,
boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
@@ -20,7 +20,6 @@ package org.apache.fineract.portfolio.loanaccount.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@@ -29,6 +28,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; 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.LoanTransactionRecoveryPaymentPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionRecoveryPaymentPreBusinessEvent; 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.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.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepository; import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
import org.apache.fineract.organisation.holiday.domain.HolidayStatusType; 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.domain.Client;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; 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.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; 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.data.PostDatedChecksStatus;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
@@ -116,12 +116,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final AccountTransferRepository accountTransferRepository; private final AccountTransferRepository accountTransferRepository;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository;
private final LoanAccrualPlatformService loanAccrualPlatformService; private final LoanAccrualPlatformService loanAccrualPlatformService;
private final PlatformSecurityContext context;
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final LoanUtilService loanUtilService; private final LoanUtilService loanUtilService;
private final StandingInstructionRepository standingInstructionRepository; private final StandingInstructionRepository standingInstructionRepository;
private final PostDatedChecksRepository postDatedChecksRepository; private final PostDatedChecksRepository postDatedChecksRepository;
private final LoanCollateralManagementRepository loanCollateralManagementRepository; private final LoanCollateralManagementRepository loanCollateralManagementRepository;
private final DelinquencyWritePlatformService delinquencyWritePlatformService;
@Transactional @Transactional
@Override @Override
@@ -157,7 +157,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment, final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment,
final String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, final String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
Boolean isHolidayValidationDone, final boolean isLoanToLoanTransfer) { Boolean isHolidayValidationDone, final boolean isLoanToLoanTransfer) {
AppUser currentUser = getAppUserIfPresent();
checkClientOrGroupActive(loan); checkClientOrGroupActive(loan);
LoanBusinessEvent repaymentEvent = getLoanRepaymentTypeBusinessEvent(repaymentTransactionType, isRecoveryRepayment, loan); LoanBusinessEvent repaymentEvent = getLoanRepaymentTypeBusinessEvent(repaymentTransactionType, isRecoveryRepayment, loan);
@@ -224,6 +223,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
recalculateAccruals(loan); recalculateAccruals(loan);
setLoanDelinquencyTag(loan, transactionDate);
if (!repaymentTransactionType.isChargeRefund()) { if (!repaymentTransactionType.isChargeRefund()) {
LoanTransactionBusinessEvent transactionRepaymentEvent = getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType, LoanTransactionBusinessEvent transactionRepaymentEvent = getTransactionRepaymentTypeBusinessEvent(repaymentTransactionType,
isRecoveryRepayment, newRepaymentTransaction); isRecoveryRepayment, newRepaymentTransaction);
@@ -536,6 +537,18 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
recalculateAccruals(loan, loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); 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 @Override
public void recalculateAccruals(Loan loan, boolean isInterestCalculationHappened) { public void recalculateAccruals(Loan loan, boolean isInterestCalculationHappened) {
LocalDate accruedTill = loan.getAccruedTill(); 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 @Override
public CommandProcessingResultBuilder creditBalanceRefund(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount, public CommandProcessingResultBuilder creditBalanceRefund(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount,
String noteText, String externalId) { String noteText, String externalId) {
@@ -720,7 +725,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
final ScheduleGeneratorDTO scheduleGeneratorDTO = null; final ScheduleGeneratorDTO scheduleGeneratorDTO = null;
AppUser appUser = getAppUserIfPresent();
final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate);
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()
&& (loan.getAccruedTill() == null || !foreClosureDate.isEqual(loan.getAccruedTill()))) { && (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()) { if (payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable).isGreaterThanZero()) {
final PaymentDetail paymentDetail = null; final PaymentDetail paymentDetail = null;
String externalId = null; String externalId = null;
final LocalDateTime currentDateTime = DateUtils.getLocalDateTimeOfTenant();
payment = LoanTransaction.repayment(loan.getOffice(), payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable), payment = LoanTransaction.repayment(loan.getOffice(), payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable),
paymentDetail, foreClosureDate, externalId); paymentDetail, foreClosureDate, externalId);
payment.updateLoan(loan); payment.updateLoan(loan);
@@ -522,6 +522,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
updateRecurringCalendarDatesForInterestRecalculation(loan); updateRecurringCalendarDatesForInterestRecalculation(loan);
this.loanAccountDomainService.recalculateAccruals(loan); this.loanAccountDomainService.recalculateAccruals(loan);
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
// Post Dated Checks // Post Dated Checks
if (command.parameterExists("postDatedChecks")) { if (command.parameterExists("postDatedChecks")) {
@@ -784,6 +785,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
updateRecurringCalendarDatesForInterestRecalculation(loan); updateRecurringCalendarDatesForInterestRecalculation(loan);
loanAccountDomainService.recalculateAccruals(loan); loanAccountDomainService.recalculateAccruals(loan);
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan));
} }
@@ -1148,6 +1150,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan); this.loanAccountDomainService.recalculateAccruals(loan);
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust); LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust);
if (newTransactionDetail.isRepaymentType() && newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency())) { if (newTransactionDetail.isRepaymentType() && newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency())) {
eventData.setNewTransactionDetail(newTransactionDetail); eventData.setNewTransactionDetail(newTransactionDetail);
@@ -1227,6 +1232,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccountDomainService.recalculateAccruals(loan); loanAccountDomainService.recalculateAccruals(loan);
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction)); businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction));
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(waiveInterestTransaction.getId()) return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(waiveInterestTransaction.getId())
.withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) .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("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("locale", command.locale()); changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat()); changes.put("dateFormat", command.dateFormat());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final Loan loan = this.loanAssembler.assembleFrom(loanId); final Loan loan = this.loanAssembler.assembleFrom(loanId);
if (command.hasParameter("writeoffReasonId")) { if (command.hasParameter("writeoffReasonId")) {
Long writeoffReasonId = command.longValueOfParameterNamed("writeoffReasonId"); Long writeoffReasonId = command.longValueOfParameterNamed("writeoffReasonId");
@@ -1290,6 +1298,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccountDomainService.recalculateAccruals(loan); loanAccountDomainService.recalculateAccruals(loan);
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff)); businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff));
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(writeOff.getId()) return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(writeOff.getId())
.withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) .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("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
changes.put("locale", command.locale()); changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat()); changes.put("dateFormat", command.dateFormat());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>();
@@ -1346,6 +1356,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
loanAccountDomainService.recalculateAccruals(loan); loanAccountDomainService.recalculateAccruals(loan);
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanCloseBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanCloseBusinessEvent(loan));
// Update loan transaction on repayment. // Update loan transaction on repayment.
@@ -2731,6 +2743,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) { && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
this.loanAccountDomainService.recalculateAccruals(loan); this.loanAccountDomainService.recalculateAccruals(loan);
} }
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanApplyOverdueChargeBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanApplyOverdueChargeBusinessEvent(loan));
} }
} }
@@ -2860,6 +2873,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (writeOffTransaction != null) { if (writeOffTransaction != null) {
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction)); businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction));
} }
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) // .withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) // .withClientId(loan.getClientId()) //
@@ -3004,6 +3018,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan); this.loanAccountDomainService.recalculateAccruals(loan);
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) // .withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) // .withClientId(loan.getClientId()) //
@@ -36,12 +36,14 @@ import org.apache.fineract.client.models.DeleteDelinquencyBucketResponse;
import org.apache.fineract.client.models.DeleteDelinquencyRangeResponse; import org.apache.fineract.client.models.DeleteDelinquencyRangeResponse;
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse; import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
import org.apache.fineract.client.models.GetDelinquencyRangesResponse; 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.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostDelinquencyBucketResponse; import org.apache.fineract.client.models.PostDelinquencyBucketResponse;
import org.apache.fineract.client.models.PostDelinquencyRangeResponse; 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.PutDelinquencyBucketResponse;
import org.apache.fineract.client.models.PutDelinquencyRangeResponse; import org.apache.fineract.client.models.PutDelinquencyRangeResponse;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
@@ -63,6 +65,7 @@ public class DelinquencyBucketsIntegrationTest {
private ResponseSpecification responseSpec; private ResponseSpecification responseSpec;
private RequestSpecification requestSpec; private RequestSpecification requestSpec;
private static final String principalAmount = "10000";
@BeforeEach @BeforeEach
public void setup() { public void setup() {
@@ -259,27 +262,22 @@ public class DelinquencyBucketsIntegrationTest {
final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec,
delinquencyBucketResponse.getResourceId()); delinquencyBucketResponse.getResourceId());
// Client and Loan account creation
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012"); final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012");
final HashMap<String, Object> loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucket.getId()); final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper,
final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap)); delinquencyBucket.getId());
assertNotNull(getLoanProductsProductResponse);
final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId);
log.info("Loan Product Bucket Name: {}", getLoanProductsProductResponse.getDelinquencyBucket().getName()); log.info("Loan Product Bucket Name: {}", getLoanProductsProductResponse.getDelinquencyBucket().getName());
assertEquals(getLoanProductsProductResponse.getDelinquencyBucket().getName(), delinquencyBucket.getName());
final String operationDate = Utils.dateFormatter.format(businessDate); final LocalDate todaysDate = Utils.getLocalDateOfTenant();
final String principalAmount = "10000"; // 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") // Create Loan Account
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("12").withRepaymentEveryAfter("1") final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(),
.withRepaymentFrequencyTypeAsMonths() // getLoanProductsProductResponse.getId().toString(), operationDate);
.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);
// When // When
// Run first time the Job // Run first time the Job
@@ -294,7 +292,8 @@ public class DelinquencyBucketsIntegrationTest {
if (getLoanRepaymentSchedule != null) { if (getLoanRepaymentSchedule != null) {
log.info("Loan with {} periods", getLoanRepaymentSchedule.getPeriods().size()); log.info("Loan with {} periods", getLoanRepaymentSchedule.getPeriods().size());
for (GetLoansLoanIdRepaymentPeriod period : getLoanRepaymentSchedule.getPeriods()) { 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); GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
} }
@Test
public void testLoanClassificationRealtime() {
// Given
final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
ArrayList<Integer> 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<GetDelinquencyTagHistoryResponse> 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<Integer> 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<GetDelinquencyTagHistoryResponse> 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<Integer> 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<GetDelinquencyTagHistoryResponse> 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<String, Object> 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());
}
}
}
} }
@@ -22,12 +22,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.linecorp.armeria.internal.shaded.guava.reflect.TypeToken;
import io.restassured.specification.RequestSpecification; import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification; import io.restassured.specification.ResponseSpecification;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Type;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
@@ -38,8 +40,10 @@ import java.util.Map;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import lombok.extern.slf4j.Slf4j; 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.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdResponse; 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.client.util.JSON;
import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.Utils;
@@ -188,6 +192,14 @@ public class LoanTransactionHelper {
return Utils.performServerGet(requestSpec, responseSpec, URL, null); return Utils.performServerGet(requestSpec, responseSpec, URL, null);
} }
public ArrayList<GetDelinquencyTagHistoryResponse> 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<ArrayList<GetDelinquencyTagHistoryResponse>>() {}.getType();
return GSON.fromJson(response, delinquencyTagsListType);
}
public Object getLoanProductDetail(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, public Object getLoanProductDetail(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final Integer loanProductId, final String jsonAttributeToGetBack) { final Integer loanProductId, final String jsonAttributeToGetBack) {
final String URL = "/fineract-provider/api/v1/loanproducts/" + loanProductId + "?associations=all&" + Utils.TENANT_IDENTIFIER; 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), ""); 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) { public HashMap makeRepaymentWithPDC(final String date, final Float amountToBePaid, final Integer loanID, final Integer paymentType) {
return (HashMap) performLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID), return (HashMap) performLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID),
getRepaymentWithPDCBodyAsJSON(date, amountToBePaid, paymentType), ""); getRepaymentWithPDCBodyAsJSON(date, amountToBePaid, paymentType), "");
@@ -785,7 +805,6 @@ public class LoanTransactionHelper {
} }
private HashMap performLoanTransaction(final String postURLForLoanTransaction, final String jsonToBeSent) { private HashMap performLoanTransaction(final String postURLForLoanTransaction, final String jsonToBeSent) {
final HashMap response = Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoanTransaction, jsonToBeSent, final HashMap response = Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoanTransaction, jsonToBeSent,
"changes"); "changes");
return (HashMap) response.get("status"); return (HashMap) response.get("status");
@@ -803,6 +822,11 @@ public class LoanTransactionHelper {
return Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoanTransaction, jsonToBeSent, responseAttribute); 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, private Object performLoanTransaction(final String postURLForLoanTransaction, final String jsonToBeSent,
ResponseSpecification responseValidationError) { ResponseSpecification responseValidationError) {