Delinquency classification including Chargeback transactions

This commit is contained in:
Jose Alberto Hernandez
2022-11-02 15:37:39 +01:00
committed by Arnold Gálovics
parent cce7902054
commit 90f1eccd4b
34 changed files with 1271 additions and 736 deletions
@@ -478,7 +478,7 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
.append(" l.closedon_date as closedOnDate,")
.append(" cbu.username as closedByUsername, cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname,")
.append(" la.overdue_since_date_derived as overdueSinceDate,")
.append(" la.overdue_since_date_derived as overdueSinceDate, ")
.append(" l.writtenoffon_date as writtenOffOnDate, l.expected_maturedon_date as expectedMaturityDate")
.append(" from m_loan l ").append("LEFT JOIN m_product_loan AS lp ON lp.id = l.product_id")
@@ -550,10 +550,7 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
final LocalDate expectedMaturityDate = JdbcSupport.getLocalDate(rs, "expectedMaturityDate");
final LocalDate overdueSinceDate = JdbcSupport.getLocalDate(rs, "overdueSinceDate");
Boolean inArrears = true;
if (overdueSinceDate == null) {
inArrears = false;
}
Boolean inArrears = (overdueSinceDate != null);
final LoanApplicationTimelineData timeline = new LoanApplicationTimelineData(submittedOnDate, submittedByUsername,
submittedByFirstname, submittedByLastname, rejectedOnDate, rejectedByUsername, rejectedByFirstname, rejectedByLastname,
@@ -22,6 +22,7 @@ import java.util.Collection;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
public interface DelinquencyReadPlatformService {
@@ -37,4 +38,6 @@ public interface DelinquencyReadPlatformService {
Collection<LoanDelinquencyTagHistoryData> retrieveDelinquencyRangeHistory(Long loanId);
CollectionData calculateLoanCollectionData(Long loanId);
}
@@ -18,10 +18,15 @@
*/
package org.apache.fineract.portfolio.delinquency.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
@@ -34,8 +39,13 @@ import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistor
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -97,4 +107,94 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
return mapperLoanDelinquencyTagHistory.map(loanDelinquencyTagData);
}
@Override
public CollectionData calculateLoanCollectionData(final Long loanId) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
final Optional<Loan> optLoan = this.loanRepository.findById(loanId);
CollectionData collectionData = CollectionData.template();
if (optLoan.isPresent()) {
final Loan loan = optLoan.get();
collectionData.setAvailableDisbursementAmount(loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()));
collectionData.setNextPaymentDueDate(loan.possibleNextRepaymentDate());
final MonetaryCurrency currency = loan.getCurrency();
BigDecimal delinquentAmount = BigDecimal.ZERO;
// Overdue Days calculation
Long overdueDays = 0L;
LocalDate overdueSinceDate = null;
final List<LoanTransaction> chargebackTransactions = loan.retrieveListOfTransactionsByType(LoanTransactionType.CHARGEBACK);
for (LoanTransaction loanTransaction : chargebackTransactions) {
Set<LoanTransactionToRepaymentScheduleMapping> loanTransactionToRepaymentScheduleMappings = loanTransaction
.getLoanTransactionToRepaymentScheduleMappings();
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = loanTransactionToRepaymentScheduleMappings
.iterator().next();
if (!loanTransactionToRepaymentScheduleMapping.getLoanRepaymentScheduleInstallment().isPrincipalCompleted(currency)) {
overdueSinceDate = loanTransaction.getTransactionDate();
break;
}
}
final List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
if (overdueSinceDate == null) {
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isNotFullyPaidOff()) {
overdueSinceDate = installment.getDueDate();
break;
}
}
}
Integer graceDays = 0;
if (overdueSinceDate != null) {
if (loan.loanProduct().getLoanProductConfigurableAttributes().getGraceOnArrearsAgingBoolean()) {
graceDays = loan.loanProduct().getLoanProductRelatedDetail().getGraceOnArrearsAgeing();
if (graceDays == null) {
graceDays = 0;
}
overdueSinceDate = overdueSinceDate.plusDays(graceDays);
}
overdueDays = DateUtils.getDifferenceInDays(overdueSinceDate, businessDate);
if (overdueDays < 0) {
overdueDays = 0L;
}
collectionData.setDelinquentDate(overdueSinceDate);
}
collectionData.setPastDueDays(overdueDays);
if (overdueDays > 0) {
collectionData.setDelinquentDays(overdueDays - graceDays);
}
final LoanTransaction lastRepaymenTransaction = loan.getLastRepaymentTransaction();
if (lastRepaymenTransaction != null) {
collectionData.setLastPaymentDate(lastRepaymenTransaction.getTransactionDate());
collectionData.setLastPaymentAmount(lastRepaymenTransaction.getAmount());
}
// Calculate Delinquency Amount
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getDueDate().isBefore(businessDate)) {
delinquentAmount = delinquentAmount.add(installment.getTotalOutstanding(currency).getAmount());
}
}
for (LoanTransaction loanTransaction : chargebackTransactions) {
Set<LoanTransactionToRepaymentScheduleMapping> loanTransactionToRepaymentScheduleMappings = loanTransaction
.getLoanTransactionToRepaymentScheduleMappings();
for (LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping : loanTransactionToRepaymentScheduleMappings) {
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanTransactionToRepaymentScheduleMapping
.getLoanRepaymentScheduleInstallment();
if (!loanRepaymentScheduleInstallment.isPrincipalCompleted(currency)) {
delinquentAmount = delinquentAmount.add(loanTransaction.getAmount());
}
}
}
collectionData.setDelinquentAmount(delinquentAmount);
}
return collectionData;
}
}
@@ -18,8 +18,10 @@
*/
package org.apache.fineract.portfolio.delinquency.service;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
public interface DelinquencyWritePlatformService {
@@ -38,12 +40,14 @@ public interface DelinquencyWritePlatformService {
CommandProcessingResult applyDelinquencyTagToLoan(Long loanId, JsonCommand command);
void applyDelinquencyTagToLoan(Long loanId, Long ageDays);
void applyDelinquencyTagToLoan(Loan loan, Long ageDays);
void removeDelinquencyTagToLoan(Loan loan);
void cleanLoanDelinquencyTags(Loan loan);
LoanScheduleDelinquencyData calculateDelinquencyData(LoanScheduleDelinquencyData loanScheduleDelinquencyData);
void applyDelinquencyTagToLoan(LoanScheduleDelinquencyData loanDelinquencyData);
LocalDate getOverdueSinceDate(Loan loan, LocalDate businessDate, Integer graceOnArrearAgeing);
}
@@ -33,6 +33,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiConstants;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
@@ -48,10 +49,13 @@ import org.apache.fineract.portfolio.delinquency.exception.DelinquencyBucketAges
import org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInvalidAgesException;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.springframework.stereotype.Service;
@@ -69,7 +73,6 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
private final LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository;
private final LoanRepositoryWrapper loanRepository;
private final LoanProductRepository loanProductRepository;
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
@Override
public CommandProcessingResult createDelinquencyRange(JsonCommand command) {
@@ -100,8 +103,9 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
"Data integrity issue with resource: " + delinquencyRange.getId());
}
repositoryRange.delete(delinquencyRange);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyRange.getId()).build();
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyRange.getId()).build();
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyRangeId).build();
}
@Override
@@ -135,7 +139,71 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
}
repositoryBucket.delete(delinquencyBucket);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyBucket.getId()).build();
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyBucketId).build();
}
@Override
public LoanScheduleDelinquencyData calculateDelinquencyData(LoanScheduleDelinquencyData loanScheduleDelinquencyData) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
Loan loan = loanScheduleDelinquencyData.getLoan();
if (loan == null) {
loan = this.loanRepository.findOneWithNotFoundDetection(loanScheduleDelinquencyData.getLoanId());
}
final Integer graceOnArrearAgeing = loan.getLoanProduct().getLoanProductRelatedDetail().getGraceOnArrearsAgeing();
final LocalDate overdueSinceDate = getOverdueSinceDate(loan, businessDate, graceOnArrearAgeing);
final Long overdueDays = calculateOverdueDays(businessDate, overdueSinceDate);
return new LoanScheduleDelinquencyData(loan.getId(), overdueSinceDate, overdueDays, loan);
}
@Override
public LocalDate getOverdueSinceDate(final Loan loan, final LocalDate businessDate, final Integer graceOnArrearAgeing) {
LoanRepaymentScheduleInstallment loanRepaymentSchedule = null;
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
if (installment.getDueDate().isBefore(businessDate) && installment.isNotFullyPaidOff()) {
loanRepaymentSchedule = installment;
break;
}
}
LocalDate overdueSinceDate = null;
final MonetaryCurrency loanCurrency = loan.getCurrency();
final List<LoanTransaction> loanTransactions = loan.retrieveListOfTransactionsByType(LoanTransactionType.CHARGEBACK);
if (loanRepaymentSchedule != null) {
// Default Due date
overdueSinceDate = loanRepaymentSchedule.getDueDate();
}
// If there is some Loan Transaction Chargeback
for (LoanTransaction loanTransaction : loanTransactions) {
if (loanTransaction.getLoanTransactionToRepaymentScheduleMappings().iterator().hasNext()) {
final LoanTransactionToRepaymentScheduleMapping transactionMapping = loanTransaction
.getLoanTransactionToRepaymentScheduleMappings().iterator().next();
if (transactionMapping != null
&& !transactionMapping.getLoanRepaymentScheduleInstallment().isPrincipalCompleted(loanCurrency)) {
overdueSinceDate = loanTransaction.getTransactionDate();
break;
}
}
}
// Include grace days If It's defined
if (overdueSinceDate != null && graceOnArrearAgeing != null) {
overdueSinceDate = overdueSinceDate.plusDays(graceOnArrearAgeing.longValue());
}
return overdueSinceDate;
}
private Long calculateOverdueDays(final LocalDate businessDate, LocalDate overdueSinceDate) {
Long overdueDays = 0L;
if (overdueSinceDate != null) {
overdueDays = DateUtils.getDifferenceInDays(overdueSinceDate, businessDate);
if (overdueDays < 0) {
overdueDays = 0L;
}
}
return overdueDays;
}
@Override
@@ -146,28 +214,31 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket();
if (delinquencyBucket != null) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
LoanRepaymentScheduleInstallment loanRepaymentSchedule = this.loanRepaymentScheduleInstallmentRepository
.findFirstByLoanAndDueDateLessThanEqualAndObligationsMetOnDateOrderByDueDate(loan, businessDate, null);
Long overdueDays = 0L;
if (loanRepaymentSchedule != null) {
overdueDays = DateUtils.getDifferenceInDays(loanRepaymentSchedule.getDueDate(), businessDate);
}
final Integer graceOnArrearAgeing = loan.getLoanProduct().getLoanProductRelatedDetail().getGraceOnArrearsAgeing();
final LocalDate overdueSinceDate = getOverdueSinceDate(loan, businessDate, graceOnArrearAgeing);
final Long overdueDays = calculateOverdueDays(businessDate, overdueSinceDate);
changes = lookUpDelinquencyRange(loan, delinquencyBucket, overdueDays);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loan.getId()).with(changes).build();
}
@Override
public void applyDelinquencyTagToLoan(Long loanId, Long ageDays) {
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
applyDelinquencyTagToLoan(loan, ageDays);
}
@Override
public void applyDelinquencyTagToLoan(final Loan loan, Long ageDays) {
public void applyDelinquencyTagToLoan(LoanScheduleDelinquencyData loanDelinquencyData) {
final Loan loan = loanDelinquencyData.getLoan();
if (loan.hasDelinquencyBucket()) {
lookUpDelinquencyRange(loan, loan.getLoanProduct().getDelinquencyBucket(), ageDays);
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket();
final Integer graceOnArrearAgeing = loan.getLoanProduct().getLoanProductRelatedDetail().getGraceOnArrearsAgeing();
LocalDate overdueSinceDate = loanDelinquencyData.getOverdueSinceDate();
if (overdueSinceDate == null) {
getOverdueSinceDate(loan, businessDate, graceOnArrearAgeing);
}
final Long overdueDays = calculateOverdueDays(businessDate, overdueSinceDate);
lookUpDelinquencyRange(loan, delinquencyBucket, overdueDays);
}
}
@@ -289,11 +360,11 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
}
}
private Map<String, Object> lookUpDelinquencyRange(final Loan loan, final DelinquencyBucket delinquencyBucket, long ageDays) {
private Map<String, Object> lookUpDelinquencyRange(final Loan loan, final DelinquencyBucket delinquencyBucket, long overdueDays) {
Map<String, Object> changes = new HashMap<>();
if (ageDays <= 0) { // No Delinquency
log.debug("Loan {} without delinquency range with {} days", loan.getId(), ageDays);
if (overdueDays <= 0) { // No Delinquency
log.debug("Loan {} without delinquency range with {} days", loan.getId(), overdueDays);
changes = setLoanDelinquencyTag(loan, null);
} else {
@@ -302,27 +373,27 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
for (final DelinquencyRange delinquencyRange : ranges) {
if (delinquencyRange.getMaximumAgeDays() == null) { // Last Range in the Bucket
if (delinquencyRange.getMinimumAgeDays() <= ageDays) {
if (delinquencyRange.getMinimumAgeDays() <= overdueDays) {
log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(),
ageDays);
overdueDays);
changes = setLoanDelinquencyTag(loan, delinquencyRange.getId());
break;
}
} else {
if (delinquencyRange.getMinimumAgeDays() <= ageDays && delinquencyRange.getMaximumAgeDays() >= ageDays) {
if (delinquencyRange.getMinimumAgeDays() <= overdueDays && delinquencyRange.getMaximumAgeDays() >= overdueDays) {
log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(),
ageDays);
overdueDays);
changes = setLoanDelinquencyTag(loan, delinquencyRange.getId());
break;
}
}
}
}
changes.put("ageDays", ageDays);
changes.put("overdueDays", overdueDays);
return changes;
}
public Map<String, Object> setLoanDelinquencyTag(Loan loan, Long delinquencyRangeId) {
private Map<String, Object> setLoanDelinquencyTag(Loan loan, Long delinquencyRangeId) {
Map<String, Object> changes = new HashMap<>();
List<LoanDelinquencyTagHistory> loanDelinquencyTagHistory = new ArrayList<>();
final LocalDate transactionDate = DateUtils.getBusinessLocalDate();
@@ -610,8 +610,8 @@ public class LoansApiResource {
if (associationParameters.contains(DataTableApiConstant.collectionAssociateParamName)) {
mandatoryResponseParameters.add(DataTableApiConstant.collectionAssociateParamName);
if (LoanStatus.fromInt(loanBasicDetails.getStatus().getId().intValue()).isActive()) {
collectionData = this.loanReadPlatformService.retrieveLoanCollectionData(loanId);
if (loanBasicDetails.isActive()) {
collectionData = this.delinquencyReadPlatformService.calculateLoanCollectionData(loanId);
}
}
}
@@ -20,45 +20,27 @@ package org.apache.fineract.portfolio.loanaccount.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Data
@NoArgsConstructor
@Accessors(chain = true)
@AllArgsConstructor
@Getter
@Setter
public final class CollectionData {
private BigDecimal availableDisbursementAmount;
private int pastDueDays;
private Long pastDueDays;
private LocalDate nextPaymentDueDate;
private int delinquentDays;
private Long delinquentDays;
private LocalDate delinquentDate;
private BigDecimal delinquentAmount;
private LocalDate lastPaymentDate;
private BigDecimal lastPaymentAmount;
private CollectionData(BigDecimal availableDisbursementAmount, int pastDueDays, LocalDate nextPaymentDueDate, int delinquentDays,
LocalDate delinquentDate, BigDecimal delinquentAmount, LocalDate lastPaymentDate, BigDecimal lastPaymentAmount) {
this.availableDisbursementAmount = availableDisbursementAmount;
this.pastDueDays = pastDueDays;
this.nextPaymentDueDate = nextPaymentDueDate;
this.delinquentDays = delinquentDays;
this.delinquentDate = delinquentDate;
this.delinquentAmount = delinquentAmount;
this.lastPaymentDate = lastPaymentDate;
this.lastPaymentAmount = lastPaymentAmount;
}
public static CollectionData instance(BigDecimal availableDisbursementAmount, int pastDueDays, LocalDate nextPaymentDueDate,
int delinquentDays, LocalDate delinquentDate, BigDecimal delinquentAmount, LocalDate lastPaymentDate,
BigDecimal lastPaymentAmount) {
return new CollectionData(availableDisbursementAmount, pastDueDays, nextPaymentDueDate, delinquentDays, delinquentDate,
delinquentAmount, lastPaymentDate, lastPaymentAmount);
}
public static CollectionData template() {
final BigDecimal zero = BigDecimal.ZERO;
return new CollectionData(zero, 0, null, 0, null, zero, null, zero);
return new CollectionData(zero, 0L, null, 0L, null, zero, null, zero);
}
}
@@ -44,6 +44,7 @@ import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData;
import org.apache.fineract.portfolio.fund.data.FundData;
import org.apache.fineract.portfolio.group.data.GroupGeneralData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.guarantor.data.GuarantorData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData;
@@ -56,6 +57,7 @@ import org.apache.fineract.portfolio.rate.data.RateData;
@Data
@NoArgsConstructor
@Accessors(chain = true)
@SuppressWarnings("ObjectToString")
public class LoanAccountData {
// basic loan details
@@ -1167,4 +1169,8 @@ public class LoanAccountData {
}
return null;
}
public boolean isActive() {
return LoanStatus.fromInt(getStatus().getId().intValue()).isActive();
}
}
@@ -22,14 +22,19 @@ import java.io.Serializable;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class LoanScheduleDelinquencyData implements Serializable {
private final Long loanId;
private final Long productId;
private final LocalDate dueDate;
private final Long ageDays;
private Long loanId;
private LocalDate overdueSinceDate;
private Long overdueDays;
private Loan loan;
}
@@ -672,7 +672,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
ChangedTransactionDetail changedTransactionDetail = null;
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
@@ -832,7 +832,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
* Consider removing this block of code or logically completing it for the future by getting the list of
* affected Transactions
***/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement,
getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
}
@@ -900,7 +900,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
* Consider removing this block of code or logically completing it for the future by getting the list of
* affected Transactions
***/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement,
getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
} else {
@@ -1087,7 +1087,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
* Consider removing this block of code or logically completing it for the future by getting the list of
* affected Transactions
***/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement,
getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
} else {
@@ -1284,7 +1284,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
* method updates accrual derived fields on installments and reverse the unprocessed transactions
*/
private void applyAccurals() {
Collection<LoanTransaction> accruals = retreiveListOfAccrualTransactions();
Collection<LoanTransaction> accruals = retrieveListOfAccrualTransactions();
if (!accruals.isEmpty()) {
if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
applyPeriodicAccruals(accruals);
@@ -2634,7 +2634,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private ChangedTransactionDetail reprocessTransactionForDisbursement() {
ChangedTransactionDetail changedTransactionDetail = null;
if (this.loanProduct.isMultiDisburseLoan()) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
@@ -3250,7 +3250,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
@@ -3300,7 +3300,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return installment;
}
private List<LoanTransaction> retreiveListOfIncomePostingTransactions() {
private List<LoanTransaction> retrieveListOfIncomePostingTransactions() {
final List<LoanTransaction> incomePostTransactions = new ArrayList<>();
List<LoanTransaction> trans = getLoanTransactions();
for (final LoanTransaction transaction : trans) {
@@ -3313,7 +3313,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return incomePostTransactions;
}
private List<LoanTransaction> retreiveListOfTransactionsPostDisbursement() {
private List<LoanTransaction> retrieveListOfTransactionsPostDisbursement() {
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
List<LoanTransaction> trans = getLoanTransactions();
for (final LoanTransaction transaction : trans) {
@@ -3326,7 +3326,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return repaymentsOrWaivers;
}
public List<LoanTransaction> retreiveListOfTransactionsPostDisbursementExcludeAccruals() {
public List<LoanTransaction> retrieveListOfTransactionsPostDisbursementExcludeAccruals() {
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed()
@@ -3340,7 +3340,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return repaymentsOrWaivers;
}
private List<LoanTransaction> retreiveListOfTransactionsExcludeAccruals() {
private List<LoanTransaction> retrieveListOfTransactionsExcludeAccruals() {
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed() && !(transaction.isAccrual() || transaction.isNonMonetaryTransaction())) {
@@ -3352,7 +3352,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return repaymentsOrWaivers;
}
private List<LoanTransaction> retreiveListOfAccrualTransactions() {
private List<LoanTransaction> retrieveListOfAccrualTransactions() {
final List<LoanTransaction> transactions = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed() && transaction.isAccrual()) {
@@ -3364,6 +3364,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return transactions;
}
public List<LoanTransaction> retrieveListOfTransactionsByType(LoanTransactionType transactionType) {
final List<LoanTransaction> transactions = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed() && transaction.getTypeOf().equals(transactionType)) {
transactions.add(transaction);
}
}
final LoanTransactionComparator transactionComparator = new LoanTransactionComparator();
Collections.sort(transactions, transactionComparator);
return transactions;
}
private boolean doPostLoanTransactionChecks(final LocalDate transactionDate,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
boolean statusChanged = false;
@@ -3403,12 +3415,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()
&& this.getStatus().isClosedObligationsMet()) {
LocalDate closedDate = this.getClosedOnDate();
reverseTransactionsOnOrAfter(retreiveListOfIncomePostingTransactions(), closedDate);
reverseTransactionsOnOrAfter(retreiveListOfAccrualTransactions(), closedDate);
reverseTransactionsOnOrAfter(retrieveListOfIncomePostingTransactions(), closedDate);
reverseTransactionsOnOrAfter(retrieveListOfAccrualTransactions(), closedDate);
HashMap<String, BigDecimal> cumulativeIncomeFromInstallments = new HashMap<>();
determineCumulativeIncomeFromInstallments(cumulativeIncomeFromInstallments);
HashMap<String, BigDecimal> cumulativeIncomeFromIncomePosting = new HashMap<>();
determineCumulativeIncomeDetails(retreiveListOfIncomePostingTransactions(), cumulativeIncomeFromIncomePosting);
determineCumulativeIncomeDetails(retrieveListOfIncomePostingTransactions(), cumulativeIncomeFromIncomePosting);
BigDecimal interestToPost = cumulativeIncomeFromInstallments.get(INTEREST)
.subtract(cumulativeIncomeFromIncomePosting.get(INTEREST));
BigDecimal feeToPost = cumulativeIncomeFromInstallments.get(FEE).subtract(cumulativeIncomeFromIncomePosting.get(FEE));
@@ -3419,7 +3431,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
interestToPost, feeToPost, penaltyToPost);
addLoanTransaction(finalIncomeTransaction);
if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
List<LoanTransaction> updatedAccrualTransactions = retreiveListOfAccrualTransactions();
List<LoanTransaction> updatedAccrualTransactions = retrieveListOfAccrualTransactions();
LocalDate lastAccruedDate = this.getDisbursementDate();
if (!updatedAccrualTransactions.isEmpty()) {
lastAccruedDate = updatedAccrualTransactions.get(updatedAccrualTransactions.size() - 1).getTransactionDate();
@@ -3646,7 +3658,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO, this);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
@@ -3789,7 +3801,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
@@ -4856,6 +4868,15 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return currentTransactionDate;
}
public LoanTransaction getLastRepaymentTransaction() {
for (final LoanTransaction loanTransaction : this.loanTransactions) {
if (!loanTransaction.isReversed() && loanTransaction.isRepaymentType()) {
return loanTransaction;
}
}
return null;
}
public LocalDate getLastUserTransactionForChargeCalc() {
LocalDate lastTransaction = getDisbursementDate();
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
@@ -5130,7 +5151,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
@@ -5243,7 +5264,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
/*
* LocalDate recalculateFrom = null; List<LoanTransaction> loanTransactions =
* this.retreiveListOfTransactionsPostDisbursementExcludeAccruals(); for (LoanTransaction loanTransaction :
* this.retrieveListOfTransactionsPostDisbursementExcludeAccruals(); for (LoanTransaction loanTransaction :
* loanTransactions) { if (recalculateFrom == null ||
* loanTransaction.getTransactionDate().isAfter(recalculateFrom)) { recalculateFrom =
* loanTransaction.getTransactionDate(); } } generatorDTO.setRecalculateFrom(recalculateFrom);
@@ -5266,7 +5287,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public ChangedTransactionDetail processTransactions() {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
@@ -5344,8 +5365,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()) {
LocalDate lastCompoundingDate = this.getDisbursementDate();
List<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = extractInterestRecalculationAdditionalDetails();
List<LoanTransaction> incomeTransactions = retreiveListOfIncomePostingTransactions();
List<LoanTransaction> accrualTransactions = retreiveListOfAccrualTransactions();
List<LoanTransaction> incomeTransactions = retrieveListOfIncomePostingTransactions();
List<LoanTransaction> accrualTransactions = retrieveListOfAccrualTransactions();
for (LoanInterestRecalcualtionAdditionalDetails compoundingDetail : compoundingDetails) {
if (!compoundingDetail.getEffectiveDate().isBefore(DateUtils.getBusinessLocalDate())) {
break;
@@ -5495,7 +5516,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public void processPostDisbursementTransactions() {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
final List<LoanTransaction> copyTransactions = new ArrayList<>();
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
for (LoanTransaction loanTransaction : allNonContraTransactionsPostDisbursement) {
@@ -5653,7 +5674,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private void updateLoanOutstandingBalances() {
Money outstanding = Money.zero(getCurrency());
List<LoanTransaction> loanTransactions = retreiveListOfTransactionsExcludeAccruals();
List<LoanTransaction> loanTransactions = retrieveListOfTransactionsExcludeAccruals();
for (LoanTransaction loanTransaction : loanTransactions) {
if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting()) {
outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency()));
@@ -6099,7 +6120,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
loanRepaymentScheduleTransactionProcessor.handleRefund(loanTransaction, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
} else {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
@@ -6191,7 +6212,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_DISBURSAL_UNDO_LAST, getDisbursementDate());
LocalDate actualDisbursementDate = null;
LocalDate lastTransactionDate = getDisbursementDate();
List<LoanTransaction> loanTransactions = retreiveListOfTransactionsExcludeAccruals();
List<LoanTransaction> loanTransactions = retrieveListOfTransactionsExcludeAccruals();
Collections.reverse(loanTransactions);
for (final LoanTransaction previousTransaction : loanTransactions) {
if (lastTransactionDate.isBefore(previousTransaction.getTransactionDate())
@@ -84,6 +84,7 @@ import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
@@ -541,13 +542,15 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
@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);
LoanScheduleDelinquencyData loanDelinquencyData = new LoanScheduleDelinquencyData(loan.getId(), transactionDate, null, loan);
loanDelinquencyData = this.delinquencyWritePlatformService.calculateDelinquencyData(loanDelinquencyData);
log.debug("Processing Loan {} with {} overdue days since date {}", loanDelinquencyData.getLoanId(),
loanDelinquencyData.getOverdueDays(), loanDelinquencyData.getOverdueSinceDate());
// Set or Unset the Delinquency Classification Tag
if (loanDelinquencyData.getOverdueDays() > 0) {
this.delinquencyWritePlatformService.applyDelinquencyTagToLoan(loanDelinquencyData);
} else {
this.delinquencyWritePlatformService.removeDelinquencyTagToLoan(loan);
this.delinquencyWritePlatformService.removeDelinquencyTagToLoan(loanDelinquencyData.getLoan());
}
}
@@ -637,7 +637,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
private void checkIfRepaymentPeriodObligationsAreMet(final LocalDate transactionDate, final MonetaryCurrency currency) {
this.obligationsMet = getTotalOutstanding(currency).isZero();
if (this.obligationsMet) {
this.obligationsMetOnDate = transactionDate;
this.obligationsMet = getCredits(currency).isZero();
if (this.obligationsMet) {
this.obligationsMetOnDate = transactionDate;
}
} else {
this.obligationsMetOnDate = null;
}
@@ -704,7 +707,6 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
this.principal = this.principal.add(transactionAmount.getAmount());
}
checkIfRepaymentPeriodObligationsAreMet(transactionDate, transactionAmount.getCurrency());
}
public void addToCredits(final BigDecimal amount) {
@@ -19,16 +19,47 @@
package org.apache.fineract.portfolio.loanaccount.domain;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
public interface LoanRepaymentScheduleInstallmentRepository
extends JpaRepository<LoanRepaymentScheduleInstallment, Long>, JpaSpecificationExecutor<LoanRepaymentScheduleInstallment> {
LoanRepaymentScheduleInstallment findFirstByLoanAndDueDateLessThanEqualAndObligationsMetOnDateOrderByDueDate(Loan loan,
LocalDate businnessDate, LocalDate obligationsMetOnDate);
@Query("""
SELECT new org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData(
lrs.loan.id,
min(lrs.dueDate),
0L,
lrs.loan
) FROM LoanRepaymentScheduleInstallment lrs
WHERE lrs.loan.loanStatus = :loanStatus AND
lrs.dueDate <= :businessDate AND
lrs.obligationsMet = :obligationsMet AND
lrs.loan.loanProduct.delinquencyBucket IS NOT NULL
GROUP BY lrs.loan
""")
Collection<LoanScheduleDelinquencyData> fetchLoanScheduleDataByDueDateAndObligationsMet(Integer loanStatus, LocalDate businessDate,
boolean obligationsMet);
@Query("""
SELECT new org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData(
lrs.loan.id,
min(lrs.dueDate),
0L,
lrs.loan
) FROM LoanRepaymentScheduleInstallment lrs
WHERE lrs.loan.loanStatus = :loanStatus AND
lrs.dueDate <= :businessDate AND
lrs.obligationsMet = :obligationsMet AND
lrs.loan.loanProduct.delinquencyBucket IS NOT NULL AND
lrs.loan.id NOT IN :loanIds
GROUP BY lrs.loan
""")
Collection<LoanScheduleDelinquencyData> fetchLoanScheduleDataByDueDateAndObligationsMet(Integer loanStatus, LocalDate businessDate,
boolean obligationsMet, List<Long> loanIds);
List<LoanRepaymentScheduleInstallment> findByLoanAndDueDateLessThanEqualAndObligationsMetOnDateOrderByDueDate(Loan loan,
LocalDate businnessDate, LocalDate obligationsMetOnDate);
}
@@ -18,11 +18,30 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Optional;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
public interface LoanTransactionRepository extends JpaRepository<LoanTransaction, Long>, JpaSpecificationExecutor<LoanTransaction> {
Optional<LoanTransaction> findByIdAndLoanId(Long transactionId, Long loanId);
@Query("""
SELECT new org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData(
lt.loan.id,
min(lt.dateOf),
0L,
lt.loan
) FROM LoanTransaction lt
WHERE lt.typeOf = :transactionType and
lt.dateOf <= :businessDate and
lt.loan.loanProduct.delinquencyBucket is not null
GROUP BY lt.loan
""")
Collection<LoanScheduleDelinquencyData> fetchLoanTransactionsByTypeAndLessOrEqualDate(Integer transactionType, LocalDate businessDate);
}
@@ -0,0 +1,29 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface LoanTransactionToRepaymentScheduleMappingRepository extends JpaRepository<LoanTransactionToRepaymentScheduleMapping, Long>,
JpaSpecificationExecutor<LoanTransactionToRepaymentScheduleMapping> {
LoanTransactionToRepaymentScheduleMapping findByLoanTransaction(LoanTransaction loanTransaction);
}
@@ -540,8 +540,8 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
pastDueDate = currentInstallment.getDueDate();
if (!currentInstallment.isAdditional() && currentInstallment.getDueDate().isAfter(transactionDate)) {
currentInstallment.addToPrincipal(transactionDate, transactionAmountUnprocessed);
currentInstallment.addToCredits(transactionAmountUnprocessed.getAmount());
currentInstallment.addToPrincipal(transactionDate, transactionAmountUnprocessed);
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
transactionAmountUnprocessed, zeroMoney, zeroMoney, zeroMoney));
@@ -21,7 +21,8 @@ package org.apache.fineract.portfolio.loanaccount.jobs.setloandelinquencytags;
import lombok.AllArgsConstructor;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
@@ -38,7 +39,8 @@ public class SetLoanDelinquencyTagsConfig {
private StepBuilderFactory steps;
private DelinquencyWritePlatformService delinquencyWritePlatformService;
private LoanReadPlatformService loanReadPlatformService;
private LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
private LoanTransactionRepository loanTransactionRepository;
@Bean
public Step setLoanDelinquencyTagsStep() {
@@ -53,7 +55,8 @@ public class SetLoanDelinquencyTagsConfig {
@Bean
public SetLoanDelinquencyTagsTasklet setLoanDelinquencyTagsTasklet() {
return new SetLoanDelinquencyTagsTasklet(delinquencyWritePlatformService, loanReadPlatformService);
return new SetLoanDelinquencyTagsTasklet(delinquencyWritePlatformService, loanRepaymentScheduleInstallmentRepository,
loanTransactionRepository);
}
}
@@ -18,14 +18,20 @@
*/
package org.apache.fineract.portfolio.loanaccount.jobs.setloandelinquencytags;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
@@ -36,24 +42,54 @@ import org.springframework.batch.repeat.RepeatStatus;
public class SetLoanDelinquencyTagsTasklet implements Tasklet {
private final DelinquencyWritePlatformService delinquencyWritePlatformService;
private final LoanReadPlatformService loanReadPlatformService;
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
private final LoanTransactionRepository loanTransactionRepository;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
log.debug("Run job for date {}", DateUtils.getBusinessLocalDate());
Collection<LoanScheduleDelinquencyData> loanScheduleDelinquencyData = this.loanReadPlatformService
.retrieveScheduleDelinquencyData(DateUtils.getBusinessLocalDate());
log.debug("Were found {} items", loanScheduleDelinquencyData.size());
for (LoanScheduleDelinquencyData loanDelinquencyData : loanScheduleDelinquencyData) {
log.debug("Processing Loan {} with due date {} and {} overdue days", loanDelinquencyData.getLoanId(),
loanDelinquencyData.getDueDate(), loanDelinquencyData.getAgeDays());
this.delinquencyWritePlatformService.applyDelinquencyTagToLoan(loanDelinquencyData.getLoanId(),
loanDelinquencyData.getAgeDays());
}
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
log.debug("Run job for date {}", businessDate);
// Read Loan Ids with Loan Transaction Charge back
Collection<LoanScheduleDelinquencyData> loanScheduleDelinquencyData = this.loanTransactionRepository
.fetchLoanTransactionsByTypeAndLessOrEqualDate(LoanTransactionType.CHARGEBACK.getValue(), businessDate);
List<Long> processedLoans = applyDelinquencyTagToLoans(loanScheduleDelinquencyData);
log.debug("{}: Records affected by setLoanDelinquencyTags: {}", ThreadLocalContextUtil.getTenant().getName(),
loanScheduleDelinquencyData.size());
processedLoans.size());
// Read Loan Ids with overdue installments
if (processedLoans.isEmpty()) {
loanScheduleDelinquencyData = this.loanRepaymentScheduleInstallmentRepository
.fetchLoanScheduleDataByDueDateAndObligationsMet(LoanStatus.ACTIVE.getValue(), businessDate, false);
} else {
loanScheduleDelinquencyData = this.loanRepaymentScheduleInstallmentRepository
.fetchLoanScheduleDataByDueDateAndObligationsMet(LoanStatus.ACTIVE.getValue(), businessDate, false, processedLoans);
}
processedLoans = applyDelinquencyTagToLoans(loanScheduleDelinquencyData);
return RepeatStatus.FINISHED;
}
private List<Long> applyDelinquencyTagToLoans(Collection<LoanScheduleDelinquencyData> loanScheduleDelinquencyData) {
List<Long> processedLoans = new ArrayList<>();
log.debug("Were found {} items", loanScheduleDelinquencyData.size());
for (LoanScheduleDelinquencyData loanDelinquencyData : loanScheduleDelinquencyData) {
// Set the data used by Delinquency Classification method
loanDelinquencyData = this.delinquencyWritePlatformService.calculateDelinquencyData(loanDelinquencyData);
log.debug("Processing Loan {} with {} overdue days since date {}", loanDelinquencyData.getLoanId(),
loanDelinquencyData.getOverdueDays(), loanDelinquencyData.getOverdueSinceDate());
// Set or Unset the Delinquency Classification Tag
if (loanDelinquencyData.getOverdueDays() > 0) {
this.delinquencyWritePlatformService.applyDelinquencyTagToLoan(loanDelinquencyData);
} else {
this.delinquencyWritePlatformService.removeDelinquencyTagToLoan(loanDelinquencyData.getLoan());
}
processedLoans.add(loanDelinquencyData.getLoanId());
}
return processedLoans;
}
}
@@ -2671,7 +2671,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
LoanScheduleDTO loanScheduleDTO = rescheduleNextInstallments(mc, loanApplicationTerms, loan, holidayDetailDTO,
loanRepaymentScheduleTransactionProcessor, onDate, calculateTill);
List<LoanTransaction> loanTransactions = loan.retreiveListOfTransactionsPostDisbursementExcludeAccruals();
List<LoanTransaction> loanTransactions = loan.retrieveListOfTransactionsPostDisbursementExcludeAccruals();
loanRepaymentScheduleTransactionProcessor.handleTransaction(loanApplicationTerms.getExpectedDisbursementDate(), loanTransactions,
currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges());
@@ -27,13 +27,11 @@ import org.apache.fineract.infrastructure.core.service.SearchParameters;
import org.apache.fineract.organisation.staff.data.StaffData;
import org.apache.fineract.portfolio.calendar.data.CalendarData;
import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionRelationData;
@@ -109,8 +107,6 @@ public interface LoanReadPlatformService {
Collection<LoanScheduleAccrualData> retriveScheduleAccrualData();
Collection<LoanScheduleDelinquencyData> retrieveScheduleDelinquencyData(LocalDate businessLocalDate);
LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId);
LoanTransactionData retrieveLoanWriteoffTemplate(Long loanId);
@@ -157,8 +153,6 @@ public interface LoanReadPlatformService {
List<LoanRepaymentScheduleInstallmentData> getRepaymentDataResponse(Long loanId);
CollectionData retrieveLoanCollectionData(Long loanId);
List<LoanTransactionRelationData> retrieveLoanTransactionRelationsByLoanTransactionId(Long loanTransactionId);
}
@@ -84,7 +84,6 @@ import org.apache.fineract.portfolio.group.data.GroupGeneralData;
import org.apache.fineract.portfolio.group.data.GroupRoleData;
import org.apache.fineract.portfolio.group.service.GroupReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
import org.apache.fineract.portfolio.loanaccount.data.LoanApplicationTimelineData;
@@ -92,7 +91,6 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData;
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData;
import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
@@ -240,7 +238,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
this.context.authenticatedUser();
final LoanScheduleResultSetExtractor fullResultsetExtractor = new LoanScheduleResultSetExtractor(
repaymentScheduleRelatedLoanData, disbursementData, isInterestRecalculationEnabled, totalPaidFeeCharges);
repaymentScheduleRelatedLoanData, disbursementData, isInterestRecalculationEnabled);
final String sql = "select " + fullResultsetExtractor.schema() + " where ls.loan_id = ? order by ls.loan_id, ls.installment";
return this.jdbcTemplate.query(sql, fullResultsetExtractor, loanId); // NOSONAR
@@ -639,7 +637,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
+ " l.loan_sub_status_id as loanSubStatusId, la.principal_overdue_derived as principalOverdue, l.is_fraud as isFraud, "
+ " la.interest_overdue_derived as interestOverdue, la.fee_charges_overdue_derived as feeChargesOverdue,"
+ " la.penalty_charges_overdue_derived as penaltyChargesOverdue, la.total_overdue_derived as totalOverdue,"
+ " la.overdue_since_date_derived as overdueSinceDate,"
+ " la.overdue_since_date_derived as overdueSinceDate, "
+ " l.sync_disbursement_with_meeting as syncDisbursementWithMeeting,"
+ " l.loan_counter as loanCounter, l.loan_product_counter as loanProductCounter,"
+ " l.is_npa as isNPA, l.days_in_month_enum as daysInMonth, l.days_in_year_enum as daysInYear, "
@@ -678,7 +676,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
+ " left join m_code_value codev on codev.id = l.writeoff_reason_cv_id"
+ " left join m_product_loan_variable_installment_config lpvi on lpvi.loan_product_id = l.product_id"
+ " left join m_loan_topup as topup on l.id = topup.loan_id"
+ " left join m_loan as topuploan on topuploan.id = topup.closure_loan_id";
+ " left join m_loan as topuploan on topuploan.id = topup.closure_loan_id ";
}
@@ -888,9 +886,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final BigDecimal totalRecovered = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalRecovered");
final LocalDate overdueSinceDate = JdbcSupport.getLocalDate(rs, "overdueSinceDate");
if (overdueSinceDate != null) {
inArrears = true;
}
inArrears = (overdueSinceDate != null);
loanSummary = new LoanSummaryData(currencyData, principalDisbursed, principalAdjustments, principalPaid,
principalWrittenOff, principalOutstanding, principalOverdue, interestCharged, interestPaid, interestWaived,
@@ -1062,10 +1058,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
private LocalDate lastDueDate;
private BigDecimal outstandingLoanPrincipalBalance;
private boolean excludePastUndisbursed;
private final BigDecimal totalPaidFeeCharges;
LoanScheduleResultSetExtractor(final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData,
Collection<DisbursementData> disbursementData, boolean isInterestRecalculationEnabled, BigDecimal totalPaidFeeCharges) {
Collection<DisbursementData> disbursementData, boolean isInterestRecalculationEnabled) {
this.currency = repaymentScheduleRelatedLoanData.getCurrency();
this.disbursement = repaymentScheduleRelatedLoanData.disbursementData();
this.totalFeeChargesDueAtDisbursement = repaymentScheduleRelatedLoanData.getTotalFeeChargesAtDisbursement();
@@ -1073,7 +1068,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
this.outstandingLoanPrincipalBalance = this.disbursement.getPrincipal();
this.disbursementData = disbursementData;
this.excludePastUndisbursed = isInterestRecalculationEnabled;
this.totalPaidFeeCharges = totalPaidFeeCharges;
}
public String schema() {
@@ -1699,25 +1693,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
}
}
@Override
public Collection<LoanScheduleDelinquencyData> retrieveScheduleDelinquencyData(LocalDate businessLocalDate) {
LoanScheduleDelinquencyMapper mapper = new LoanScheduleDelinquencyMapper(DateUtils.getBusinessLocalDate());
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("select ").append(mapper.schema())
// Just get the overdued installments
.append(" where loan.loan_status_id=:active and ls.duedate <= :businessLocalDate and ")
// Just get the unpaid installments using the completed_derived flag
.append(" ls.completed_derived = false and ")
// Just the Loan Product linked to delinquency bucket
.append(" mpl.delinquency_bucket_id is not null ");
sqlBuilder.append(" group by ls.loan_id, loan.product_id ");
Map<String, Object> paramMap = new HashMap<>(3);
paramMap.put("active", LoanStatus.ACTIVE.getValue());
paramMap.put("businessLocalDate", businessLocalDate);
return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper);
}
@Override
public Collection<LoanScheduleAccrualData> retriveScheduleAccrualData() {
@@ -1887,33 +1862,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
}
}
private static final class LoanScheduleDelinquencyMapper implements RowMapper<LoanScheduleDelinquencyData> {
private LocalDate businessDate;
LoanScheduleDelinquencyMapper(LocalDate businessDate) {
this.businessDate = businessDate;
}
public String schema() {
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append(" ls.loan_id as loanId, loan.product_id as productId, min(ls.duedate) as duedate ")
.append(" from m_loan_repayment_schedule ls left join m_loan loan on loan.id=ls.loan_id ")
.append(" left join m_product_loan mpl on mpl.id = loan.product_id ");
return sqlBuilder.toString();
}
@Override
public LoanScheduleDelinquencyData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
final Long loanId = rs.getLong("loanId");
final Long productId = rs.getLong("productId");
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "duedate");
final long ageDays = DateUtils.getDifferenceInDays(dueDate, businessDate);
return new LoanScheduleDelinquencyData(loanId, productId, dueDate, ageDays);
}
}
@Override
public LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId) {
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
@@ -2283,7 +2231,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
@Override
public Collection<Long> retrieveLoanIdsWithPendingIncomePostingTransactions() {
LocalDate currentdate = DateUtils.getBusinessLocalDate();
StringBuilder sqlBuilder = new StringBuilder().append(" select distinct loan.id ").append(" from m_loan as loan ").append(
StringBuilder sqlBuilder = new StringBuilder().append(" select distinct loan.id from m_loan as loan ").append(
" inner join m_loan_recalculation_details as recdet on (recdet.loan_id = loan.id and recdet.is_compounding_to_be_posted_as_transaction is not null and recdet.is_compounding_to_be_posted_as_transaction = true) ")
.append(" inner join m_loan_repayment_schedule as repsch on repsch.loan_id = loan.id ")
.append(" inner join m_loan_interest_recalculation_additional_details as adddet on adddet.loan_repayment_schedule_id = repsch.id ")
@@ -2292,7 +2240,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
.append(" and adddet.effective_date is not null ").append(" and trans.transaction_date is null ")
.append(" and adddet.effective_date < ? ");
try {
return this.jdbcTemplate.queryForList(sqlBuilder.toString(), new Object[] { currentdate }, Long.class);
return this.jdbcTemplate.queryForList(sqlBuilder.toString(), Long.class, new Object[] { currentdate });
} catch (final EmptyResultDataAccessException e) {
return null;
}
@@ -2438,62 +2386,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
return this.jdbcTemplate.queryForObject(sql, Integer.class);
}
@Override
public CollectionData retrieveLoanCollectionData(Long loanId) {
final CollectionDataMapper mapper = new CollectionDataMapper(sqlGenerator);
String sql = "select " + mapper.schema();
CollectionData collectionData = this.jdbcTemplate.queryForObject(sql, mapper, loanId, loanId); // NOSONAR
return collectionData;
}
private static final class CollectionDataMapper implements RowMapper<CollectionData> {
private final DatabaseSpecificSQLGenerator sqlGenerator;
CollectionDataMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
this.sqlGenerator = sqlGenerator;
}
public String schema() {
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append(
"l.id as loanId, coalesce((l.approved_principal - l.principal_disbursed_derived), 0) as availableDisbursementAmount, ");
sqlBuilder.append(
sqlGenerator.dateDiff(sqlGenerator.currentBusinessDate(), "laa.overdue_since_date_derived") + " as pastDueDays, ");
sqlBuilder.append(
"(select coalesce(min(lrs.duedate), null) as duedate from m_loan_repayment_schedule lrs where lrs.loan_id=l.id and lrs.completed_derived is false and lrs.duedate >= "
+ sqlGenerator.currentBusinessDate() + ") as nextPaymentDueDate, ");
sqlBuilder.append(
sqlGenerator.dateDiff(sqlGenerator.currentBusinessDate(), "laa.overdue_since_date_derived") + " as delinquentDays, ");
sqlBuilder.append(sqlGenerator.currentBusinessDate()
+ " as delinquentDate, coalesce(laa.total_overdue_derived, 0) as delinquentAmount, ");
sqlBuilder.append("lre.transactionDate as lastPaymentDate, coalesce(lre.amount, 0) as lastPaymentAmount ");
sqlBuilder.append("from m_loan l left join m_loan_arrears_aging laa on laa.loan_id = l.id ");
sqlBuilder.append(
"left join (select lt.loan_id, lt.transaction_date as transactionDate, lt.amount as amount from m_loan_transaction lt ");
sqlBuilder.append(
"where lt.loan_id =? and lt.is_reversed = false and lt.transaction_type_enum=2 order by lt.transaction_date desc, lt.id desc limit 1) lre on lre.loan_id = l.id ");
sqlBuilder.append("where l.id=? ");
return sqlBuilder.toString();
}
@Override
public CollectionData mapRow(ResultSet rs, int rowNum) throws SQLException {
final LocalDate nextPaymentDueDate = JdbcSupport.getLocalDate(rs, "nextPaymentDueDate");
final LocalDate delinquentDate = JdbcSupport.getLocalDate(rs, "delinquentDate");
final LocalDate lastPaymentDate = JdbcSupport.getLocalDate(rs, "lastPaymentDate");
final BigDecimal availableDisbursementAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "availableDisbursementAmount");
final BigDecimal delinquentAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "delinquentAmount");
final BigDecimal lastPaymentAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "lastPaymentAmount");
final int pastDueDays = rs.getInt("pastDueDays");
final int delinquentDays = rs.getInt("delinquentDays");
return CollectionData.instance(availableDisbursementAmount, pastDueDays, nextPaymentDueDate, delinquentDays, delinquentDate,
delinquentAmount, lastPaymentDate, lastPaymentAmount);
}
}
@Override
public List<LoanTransactionRelationData> retrieveLoanTransactionRelationsByLoanTransactionId(Long loanTransactionId) {
final LoanTransaction loanTransaction = this.loanTransactionRepository.getReferenceById(loanTransactionId);
@@ -2501,4 +2393,5 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
.findByFromTransactionAndRelationType(loanTransaction, LoanTransactionRelationTypeEnum.CHARGEBACK);
return loanTransactionRelationMapper.map(loanTransactionRelations);
}
}
@@ -1289,7 +1289,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
this.loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate);
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargebackTransactionBusinessEvent(loanTransaction));
@@ -388,7 +388,7 @@ public class FixedDepositAccount extends SavingsAccount {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
final List<SavingsAccountTransaction> savingsAccountTransactions = retreiveListOfTransactions();
final List<SavingsAccountTransaction> savingsAccountTransactions = retrieveListOfTransactions();
if (savingsAccountTransactions.size() > 0) {
final SavingsAccountTransaction accountTransaction = savingsAccountTransactions.get(savingsAccountTransactions.size() - 1);
if (accountTransaction.isAfter(closedDate)) {
@@ -473,7 +473,7 @@ public class FixedDepositAccount extends SavingsAccount {
}
}
final List<SavingsAccountTransaction> savingsAccountTransactions = retreiveListOfTransactions();
final List<SavingsAccountTransaction> savingsAccountTransactions = retrieveListOfTransactions();
if (savingsAccountTransactions.size() > 0) {
final SavingsAccountTransaction accountTransaction = savingsAccountTransactions.get(savingsAccountTransactions.size() - 1);
if (accountTransaction.isAfter(closedDate)) {
@@ -492,7 +492,7 @@ public class RecurringDepositAccount extends SavingsAccount {
}
}
final List<SavingsAccountTransaction> savingsAccountTransactions = retreiveListOfTransactions();
final List<SavingsAccountTransaction> savingsAccountTransactions = retrieveListOfTransactions();
if (savingsAccountTransactions.size() > 0) {
final SavingsAccountTransaction accountTransaction = savingsAccountTransactions.get(savingsAccountTransactions.size() - 1);
if (accountTransaction.isAfter(closedDate)) {
@@ -593,7 +593,7 @@ public class RecurringDepositAccount extends SavingsAccount {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
final List<SavingsAccountTransaction> savingsAccountTransactions = retreiveListOfTransactions();
final List<SavingsAccountTransaction> savingsAccountTransactions = retrieveListOfTransactions();
if (savingsAccountTransactions.size() > 0) {
final SavingsAccountTransaction accountTransaction = savingsAccountTransactions.get(savingsAccountTransactions.size() - 1);
if (accountTransaction.isAfter(closedDate)) {
@@ -934,7 +934,7 @@ public class RecurringDepositAccount extends SavingsAccount {
}
private List<SavingsAccountTransaction> retreiveOrderedDepositTransactions() {
final List<SavingsAccountTransaction> listOfTransactionsSorted = retreiveListOfTransactions();
final List<SavingsAccountTransaction> listOfTransactionsSorted = retrieveListOfTransactions();
final List<SavingsAccountTransaction> orderedDepositTransactions = new ArrayList<>();
@@ -928,7 +928,7 @@ public class SavingsAccount extends AbstractPersistableCustom {
}
protected List<SavingsAccountTransaction> retreiveOrderedNonInterestPostingTransactions() {
final List<SavingsAccountTransaction> listOfTransactionsSorted = retreiveListOfTransactions();
final List<SavingsAccountTransaction> listOfTransactionsSorted = retrieveListOfTransactions();
final List<SavingsAccountTransaction> orderedNonInterestPostingTransactions = new ArrayList<>();
@@ -965,7 +965,7 @@ public class SavingsAccount extends AbstractPersistableCustom {
return listOfTransactionsSorted;
}
protected List<SavingsAccountTransaction> retreiveListOfTransactions() {
protected List<SavingsAccountTransaction> retrieveListOfTransactions() {
final List<SavingsAccountTransaction> listOfTransactionsSorted = new ArrayList<>();
listOfTransactionsSorted.addAll(this.transactions);
@@ -983,7 +983,7 @@ public class SavingsAccount extends AbstractPersistableCustom {
if (backdatedTxnsAllowedTill) {
accountTransactionsSorted = retrieveSortedTransactions();
} else {
accountTransactionsSorted = retreiveListOfTransactions();
accountTransactionsSorted = retrieveListOfTransactions();
}
boolean isTransactionsModified = false;
@@ -1058,7 +1058,7 @@ public class SavingsAccount extends AbstractPersistableCustom {
if (backdatedTxnsAllowedTill) {
accountTransactionsSorted = retrieveSortedTransactions();
} else {
accountTransactionsSorted = retreiveListOfTransactions();
accountTransactionsSorted = retrieveListOfTransactions();
}
}
resetAccountTransactionsEndOfDayBalances(accountTransactionsSorted, interestPostingUpToDate);
@@ -1415,7 +1415,7 @@ public class SavingsAccount extends AbstractPersistableCustom {
boolean transactionBeforeLastInterestPosting = false;
if (!backdatedTxnsAllowedTill) {
for (final SavingsAccountTransaction transaction : retreiveListOfTransactions()) {
for (final SavingsAccountTransaction transaction : retrieveListOfTransactions()) {
if ((transaction.isInterestPostingAndNotReversed() || transaction.isOverdraftInterestAndNotReversed())
&& transaction.isAfter(transactionDate) && !transaction.isReversalTransaction()) {
transactionBeforeLastInterestPosting = true;
@@ -1441,7 +1441,7 @@ public class SavingsAccount extends AbstractPersistableCustom {
if (backdatedTxnsAllowedTill) {
transactionsSortedByDate = retrieveSortedTransactions();
} else {
transactionsSortedByDate = retreiveListOfTransactions();
transactionsSortedByDate = retrieveListOfTransactions();
}
Money runningBalance = Money.zero(this.currency);
@@ -1524,7 +1524,7 @@ public class SavingsAccount extends AbstractPersistableCustom {
if (backdatedTxnsAllowedTill) {
transactionsSortedByDate = retrieveSortedTransactions();
} else {
transactionsSortedByDate = retreiveListOfTransactions();
transactionsSortedByDate = retrieveListOfTransactions();
}
Money runningBalance = Money.zero(this.currency);
@@ -2507,7 +2507,7 @@ public class SavingsAccount extends AbstractPersistableCustom {
}
public void validateAccountBalanceDoesNotBecomeNegativeMinimal(final BigDecimal transactionAmount, final boolean isException) {
// final List<SavingsAccountTransaction> transactionsSortedByDate = retreiveListOfTransactions();
// final List<SavingsAccountTransaction> transactionsSortedByDate = retrieveListOfTransactions();
Money runningBalance = this.summary.getAccountBalance(getCurrency());
Money minRequiredBalance = minRequiredBalanceDerived(getCurrency());
org.joda.time.LocalDate lastSavingsDate = null;
@@ -2846,7 +2846,7 @@ public class SavingsAccount extends AbstractPersistableCustom {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
final List<SavingsAccountTransaction> savingsAccountTransactions = retreiveListOfTransactions();
final List<SavingsAccountTransaction> savingsAccountTransactions = retrieveListOfTransactions();
if (savingsAccountTransactions.size() > 0) {
final SavingsAccountTransaction accountTransaction = savingsAccountTransactions.get(savingsAccountTransactions.size() - 1);
if (accountTransaction.isAfter(closedDate)) {
@@ -3806,7 +3806,7 @@ public class SavingsAccount extends AbstractPersistableCustom {
}
public LocalDate retrieveLastTransactionDate() {
final List<SavingsAccountTransaction> transactionsSortedByDate = retreiveListOfTransactions();
final List<SavingsAccountTransaction> transactionsSortedByDate = retrieveListOfTransactions();
SavingsAccountTransaction lastTransaction = null;
if (transactionsSortedByDate.size() > 0) {
lastTransaction = transactionsSortedByDate.get(transactionsSortedByDate.size() - 1);
@@ -305,7 +305,7 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
}
private List<SavingsAccountTransactionData> retreiveOrderedNonInterestPostingTransactions(final SavingsAccountData savingsAccountData) {
final List<SavingsAccountTransactionData> listOfTransactionsSorted = retreiveListOfTransactions(savingsAccountData);
final List<SavingsAccountTransactionData> listOfTransactionsSorted = retrieveListOfTransactions(savingsAccountData);
final List<SavingsAccountTransactionData> orderedNonInterestPostingTransactions = new ArrayList<>();
@@ -319,7 +319,7 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
return orderedNonInterestPostingTransactions;
}
private List<SavingsAccountTransactionData> retreiveListOfTransactions(final SavingsAccountData savingsAccountData) {
private List<SavingsAccountTransactionData> retrieveListOfTransactions(final SavingsAccountData savingsAccountData) {
final List<SavingsAccountTransactionData> listOfTransactionsSorted = new ArrayList<>();
listOfTransactionsSorted.addAll(savingsAccountData.getSavingsAccountTransactionData());
@@ -407,7 +407,7 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
Money runningBalance = openingAccountBalance.copy();
List<SavingsAccountTransactionData> accountTransactionsSorted = retreiveListOfTransactions(savingsAccountData);
List<SavingsAccountTransactionData> accountTransactionsSorted = retrieveListOfTransactions(savingsAccountData);
boolean isTransactionsModified = false;
for (final SavingsAccountTransactionData transaction : accountTransactionsSorted) {
@@ -466,7 +466,7 @@ public class SavingsAccountInterestPostingServiceImpl implements SavingsAccountI
}
if (isTransactionsModified) {
accountTransactionsSorted = retreiveListOfTransactions(savingsAccountData);
accountTransactionsSorted = retrieveListOfTransactions(savingsAccountData);
}
resetAccountTransactionsEndOfDayBalances(accountTransactionsSorted, interestPostingUpToDate, savingsAccountData);
}
@@ -85,4 +85,5 @@
<include file="parts/0063_add_permissions_for_external_event_configuration.xml" relativeToChangelogFile="true"/>
<include file="parts/0064_refactor_loan_transaction_strategy.xml" relativeToChangelogFile="true"/>
<include file="parts/0065_add_bypass_loan_write_transaction_permission.xml" relativeToChangelogFile="true"/>
<include file="parts/0066_delinquency_classification_chargeback.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="1" author="fineract">
<createIndex indexName="ind_m_loan_delinquency_tag_history_loan_id" tableName="m_loan_delinquency_tag_history">
<column name="loan_id"/>
</createIndex>
<createIndex indexName="ind_m_loan_delinquency_tag_history_liftedon_date" tableName="m_loan_delinquency_tag_history">
<column name="liftedon_date"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,192 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.integrationtests;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.time.LocalDate;
import java.util.HashMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
import org.apache.fineract.client.models.GetDelinquencyRangesResponse;
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.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@Slf4j
public class DelinquencyAndChargebackIntegrationTest {
private ResponseSpecification responseSpec;
private RequestSpecification requestSpec;
private LoanTransactionHelper loanTransactionHelper;
private static final String principalAmount = "1200.00";
private static final Double doubleZERO = Double.valueOf("0.00");
@BeforeEach
public void setup() {
Utils.initializeRESTAssured();
requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
}
@Test
public void testLoanClassificationStepAsPartOfCOB() {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
final LocalDate todaysDate = Utils.getLocalDateOfTenant();
LocalDate businessDate = todaysDate.minusDays(57);
log.info("Current Business date {}", businessDate);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate);
final SchedulerJobHelper schedulerJobHelper = new SchedulerJobHelper(requestSpec);
// Delinquency Bucket
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec,
delinquencyBucketId);
// 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());
// Older date to have more than one overdue installment
LocalDate transactionDate = businessDate;
String operationDate = Utils.dateFormatter.format(transactionDate);
log.info("Operation date {}", transactionDate);
// Create Loan Account
final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(),
getLoanProductsProductResponse.getId().toString(), operationDate);
// Run first time the Loan COB Job
final String jobName = "Loan COB";
schedulerJobHelper.executeAndAwaitJob(jobName);
// Get loan details expecting to have not a delinquency classification
GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
validateLoanAccount(getLoansLoanIdResponse, "0.00", principalAmount, 0, doubleZERO);
// Move the Business date to get older the loan and to have an overdue loan
businessDate = businessDate.plusDays(43);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate);
log.info("Current Business date {}", businessDate);
String amountVal = "100.00";
Float transactionAmount = Float.valueOf(amountVal);
PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate,
transactionAmount, loanId);
assertNotNull(loanIdTransactionsResponse);
final Integer transactionId = loanIdTransactionsResponse.getResourceId();
loanTransactionHelper.reviewLoanTransactionRelations(loanId, transactionId, 0);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
validateLoanAccount(getLoansLoanIdResponse, "0.00", "1100.00", 0, doubleZERO);
final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, amountVal, 0,
responseSpec);
loanTransactionHelper.reviewLoanTransactionRelations(loanId, transactionId, 1);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
// Past Due Days in Zero because the Charge back transaction exists and It was done with the current date
validateLoanAccount(getLoansLoanIdResponse, amountVal, principalAmount, 0, Double.valueOf("100.00"));
// Move the Business date to get older the loan and to have an overdue loan
businessDate = businessDate.plusDays(9);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate);
log.info("Current Business date {}", businessDate);
// Run Second time the Job
schedulerJobHelper.executeAndAwaitJob(jobName);
// Get loan details expecting to have a delinquency classification
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
// The value is lower due the 3 grace days
validateLoanAccount(getLoansLoanIdResponse, "100.00", principalAmount, 9, Double.valueOf("100.00"));
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
}
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("0") //
.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 GetDelinquencyRangesResponse validateLoanAccount(GetLoansLoanIdResponse getLoansLoanIdResponse, final String adjustments,
final String outstanding, Integer pastDueDays, Double delinquentAmount) {
assertNotNull(getLoansLoanIdResponse);
final GetDelinquencyRangesResponse delinquencyRange = getLoansLoanIdResponse.getDelinquencyRange();
log.info("Loan Delinquency Range is null {}", (delinquencyRange == null));
if (delinquencyRange != null) {
log.info("Loan Delinquency Range is {}", delinquencyRange.getClassification());
}
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
loanTransactionHelper.evaluateLoanSummaryAdjustments(getLoansLoanIdResponse, Double.valueOf(adjustments));
DelinquencyBucketsHelper.evaluateLoanCollectionData(getLoansLoanIdResponse, pastDueDays, delinquentAmount);
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf(outstanding));
return delinquencyRange;
}
}
@@ -234,12 +234,394 @@ public class DelinquencyBucketsIntegrationTest {
DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, response403Spec, jsonBucket);
}
@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(50);
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);
loanTransactionHelper.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 testLoanClassificationRealtimeWithCharges() {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
// 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
LocalDate transactionDate = todaysDate.minusMonths(2).minusDays(5);
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);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
// Apply a repayment to get a full paid installment
operationDate = Utils.dateFormatter.format(todaysDate);
loanTransactionHelper.makeLoanRepayment(operationDate, 2049.99f, loanId);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
// The Loan Delinquency Classification after Repayment command must be null
log.info("Loan Delinquency Range after Repayment {}", getLoansLoanIdResponse.getDelinquencyRange());
assertNull(getLoansLoanIdResponse.getDelinquencyRange());
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
transactionDate = todaysDate.minusDays(18);
operationDate = Utils.dateFormatter.format(transactionDate);
// Create and apply Charge for Specific Due Date
final Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanSpecifiedDueDateJSON(1, "30", false));
assertNotNull(chargeId);
final Integer loanChargeId = loanTransactionHelper.addChargesForLoan(loanId, getChargeApplyJSON(chargeId, operationDate),
responseSpec);
assertNotNull(loanChargeId);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
log.info("Loan Delinquency Range after add Loan Charge {}", getLoansLoanIdResponse.getDelinquencyRange());
assertNotNull(getLoansLoanIdResponse.getDelinquencyRange());
// Evaluate a Delinquency Tag set after add charge to the Loan
assertEquals(getLoansLoanIdResponse.getDelinquencyRange().getClassification(), classificationExpected);
}
@Test
public void testLoanClassificationRealtimeOlderLoan() {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
// 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
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);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
// Apply a repayment to get a first full paid installment
transactionDate = todaysDate.minusDays(1);
operationDate = Utils.dateFormatter.format(transactionDate);
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();
log.info("Local date of Tenant: {}", todaysDate);
// Older date to have more than one overdue installment
final LocalDate transactionDate = todaysDate.minusDays(50);
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);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
log.info("Loan Delinquency Range after Disbursement in null? {}", (getLoansLoanIdResponse.getDelinquencyRange() == null));
assertNotNull(getLoansLoanIdResponse);
assertNotNull(getLoansLoanIdResponse.getDelinquencyRange());
log.info("Loan Delinquency Range after Disbursement {}", getLoansLoanIdResponse.getDelinquencyRange());
// First Loan Delinquency Classification after Disbursement command
assertEquals(getLoansLoanIdResponse.getDelinquencyRange().getClassification(), classificationExpected);
loanTransactionHelper.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.reverseLoanTransaction(loanId,
loansLoanIdTransactions.getResourceId(), operationDate, responseSpec);
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());
}
@Test
public void testLoanClassificationJob() {
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
LocalDate businessDate = Utils.getLocalDateOfTenant();
businessDate = businessDate.minusDays(57);
businessDate = businessDate.minusDays(37);
log.info("Current date {}", businessDate);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate);
@@ -277,7 +659,7 @@ public class DelinquencyBucketsIntegrationTest {
final LocalDate todaysDate = Utils.getLocalDateOfTenant();
// Older date to have more than one overdue installment
final LocalDate transactionDate = todaysDate.minusDays(65);
final LocalDate transactionDate = todaysDate.minusDays(57);
String operationDate = Utils.dateFormatter.format(transactionDate);
// Create Loan Account
@@ -290,19 +672,13 @@ public class DelinquencyBucketsIntegrationTest {
// Get loan details expecting to have not a delinquency classification
GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
final GetDelinquencyRangesResponse firstTestCase = getLoansLoanIdResponse.getDelinquencyRange();
log.info("Loan Delinquency Range is null {}", (firstTestCase == null));
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());
}
}
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
// Move the Business date to get older the loan and to have an overdue loan
businessDate = businessDate.plusDays(40);
businessDate = businessDate.plusMonths(1);
log.info("Current date {}", businessDate);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate);
// Run Second time the Job
@@ -310,7 +686,11 @@ public class DelinquencyBucketsIntegrationTest {
// Get loan details expecting to have a delinquency classification
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
final GetDelinquencyRangesResponse secondTestCase = getLoansLoanIdResponse.getDelinquencyRange();
assertNotNull(secondTestCase);
log.info("Loan Delinquency Range is {}", secondTestCase.getClassification());
// Then
@@ -318,7 +698,6 @@ public class DelinquencyBucketsIntegrationTest {
assertNotNull(getLoanProductsProductResponse);
assertNull(firstTestCase);
assertEquals(getLoanProductsProductResponse.getDelinquencyBucket().getName(), delinquencyBucket.getName());
assertNotNull(secondTestCase);
assertEquals(secondTestCase.getClassification(), classificationExpected);
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
@@ -402,7 +781,7 @@ public class DelinquencyBucketsIntegrationTest {
}
// Move the Business date to get older the loan and to have an overdue loan
businessDate = businessDate.plusDays(40);
businessDate = businessDate.plusDays(50);
log.info("Current date {}", businessDate);
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, businessDate);
// Run Second time the Job
@@ -410,7 +789,9 @@ public class DelinquencyBucketsIntegrationTest {
// Get loan details expecting to have a delinquency classification
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
final GetDelinquencyRangesResponse secondTestCase = getLoansLoanIdResponse.getDelinquencyRange();
assertNotNull(secondTestCase);
log.info("Loan Delinquency Range is {}", secondTestCase.getClassification());
// Then
@@ -418,384 +799,11 @@ public class DelinquencyBucketsIntegrationTest {
assertNotNull(getLoanProductsProductResponse);
assertNull(firstTestCase);
assertEquals(getLoanProductsProductResponse.getDelinquencyBucket().getName(), delinquencyBucket.getName());
assertNotNull(secondTestCase);
assertEquals(secondTestCase.getClassification(), classificationExpected);
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);
loanTransactionHelper.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 testLoanClassificationRealtimeWithCharges() {
// 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
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 repayment to get a full paid installment
operationDate = Utils.dateFormatter.format(todaysDate);
loanTransactionHelper.makeLoanRepayment(operationDate, 1033.33f, 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());
printRepaymentSchedule(getLoansLoanIdResponse);
transactionDate = todaysDate.minusDays(18);
operationDate = Utils.dateFormatter.format(transactionDate);
// Create and apply Charge for Specific Due Date
final Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanSpecifiedDueDateJSON(1, "30", false));
assertNotNull(chargeId);
final Integer loanChargeId = loanTransactionHelper.addChargesForLoan(loanId, getChargeApplyJSON(chargeId, operationDate),
responseSpec);
assertNotNull(loanChargeId);
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
printRepaymentSchedule(getLoansLoanIdResponse);
log.info("Loan Delinquency Range after add Loan Charge {}", getLoansLoanIdResponse.getDelinquencyRange());
assertNotNull(getLoansLoanIdResponse.getDelinquencyRange());
// Evaluate a Delinquency Tag set after add charge to the Loan
assertEquals(getLoansLoanIdResponse.getDelinquencyRange().getClassification(), classificationExpected);
}
@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);
loanTransactionHelper.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);
loanTransactionHelper.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.reverseLoanTransaction(loanId,
loansLoanIdTransactions.getResourceId(), operationDate, responseSpec);
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);
@@ -819,17 +827,6 @@ public class DelinquencyBucketsIntegrationTest {
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());
}
}
}
private String getChargeApplyJSON(final Integer chargeId, final String dueDate) {
final HashMap<String, Object> map = new HashMap<>();
map.put("chargeId", chargeId);
@@ -20,9 +20,8 @@ package org.apache.fineract.integrationtests;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertNull;
import com.google.gson.Gson;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
@@ -30,26 +29,23 @@ import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
import org.apache.fineract.client.models.GetDelinquencyRangesResponse;
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.GetLoansLoanIdSummary;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.GetPaymentTypesResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.PaymentTypeHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -99,7 +95,8 @@ public class LoanTransactionChargebackTest {
reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00"));
final Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "1000.00", 0, responseSpec);
final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "1000.00", 0,
responseSpec);
reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00"));
reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("1000.00"));
@@ -141,7 +138,8 @@ public class LoanTransactionChargebackTest {
final Integer transactionId = loanIdTransactionsResponse.getResourceId();
reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("666.67"));
final Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, amount.toString(), 0, responseSpec);
final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, amount.toString(),
0, responseSpec);
reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("666.67"));
reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("1000.00"));
@@ -160,7 +158,8 @@ public class LoanTransactionChargebackTest {
}
}
evaluateLoanSummaryAdjustments(getLoansLoanIdResponse, Double.valueOf(baseAmount));
loanTransactionHelper.evaluateLoanSummaryAdjustments(getLoansLoanIdResponse, Double.valueOf(baseAmount));
DelinquencyBucketsHelper.evaluateLoanCollectionData(getLoansLoanIdResponse, 0, Double.valueOf("333.33"));
}
@Test
@@ -179,7 +178,7 @@ public class LoanTransactionChargebackTest {
log.info("Try to apply the Charge back over transaction Id {} with type {}", loanTransaction.getId(),
loanTransaction.getType().getCode());
applyChargebackTransaction(loanId, loanTransaction.getId().intValue(), amountVal, 0, responseSpecError);
loanTransactionHelper.applyChargebackTransaction(loanId, loanTransaction.getId().intValue(), amountVal, 0, responseSpecError);
}
@Test
@@ -191,6 +190,11 @@ public class LoanTransactionChargebackTest {
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
GetDelinquencyRangesResponse delinquencyRange = getLoansLoanIdResponse.getDelinquencyRange();
assertNotNull(delinquencyRange);
log.info("Loan Delinquency Range is {}", delinquencyRange.getClassification());
GetLoansLoanIdRepaymentSchedule getLoanRepaymentSchedule = getLoansLoanIdResponse.getRepaymentSchedule();
log.info("Loan with {} periods", getLoanRepaymentSchedule.getPeriods().size());
assertEquals(2, getLoanRepaymentSchedule.getPeriods().size());
@@ -207,7 +211,8 @@ public class LoanTransactionChargebackTest {
reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00"));
Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "500.00", 0, responseSpec);
Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "500.00", 0,
responseSpec);
reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00"));
reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("500.00"));
@@ -232,7 +237,7 @@ public class LoanTransactionChargebackTest {
}
}
chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "300.00", 0, responseSpec);
chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "300.00", 0, responseSpec);
reviewLoanTransactionRelations(loanId, transactionId, 2, Double.valueOf("0.00"));
reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("800.00"));
@@ -241,6 +246,10 @@ public class LoanTransactionChargebackTest {
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.active");
delinquencyRange = getLoansLoanIdResponse.getDelinquencyRange();
assertNull(delinquencyRange);
log.info("Loan Delinquency Range is null {}", (delinquencyRange == null));
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("800.00"));
// N+1 Scenario -- Remains the same periods number
@@ -280,7 +289,8 @@ public class LoanTransactionChargebackTest {
reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00"));
final Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "200.00", 0, responseSpec);
final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "200.00", 0,
responseSpec);
reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00"));
reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("100.00"));
@@ -290,6 +300,10 @@ public class LoanTransactionChargebackTest {
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.active");
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("100.00"));
final GetDelinquencyRangesResponse delinquencyRange = getLoansLoanIdResponse.getDelinquencyRange();
assertNull(delinquencyRange);
log.info("Loan Delinquency Range is null {}", (delinquencyRange == null));
}
@Test
@@ -314,7 +328,8 @@ public class LoanTransactionChargebackTest {
reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00"));
final Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "100.00", 0, responseSpec);
final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "100.00", 0,
responseSpec);
reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00"));
reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("0.00"));
@@ -348,8 +363,11 @@ public class LoanTransactionChargebackTest {
reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00"));
final Integer chargebackTransactionId = applyChargebackTransaction(loanId, transactionId, "50.00", 0, responseSpec);
GetDelinquencyRangesResponse delinquencyRange = getLoansLoanIdResponse.getDelinquencyRange();
assertNull(delinquencyRange);
log.info("Loan Delinquency Range is null {}", (delinquencyRange == null));
final Integer chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "50.00", 0,
responseSpec);
reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00"));
reviewLoanTransactionRelations(loanId, chargebackTransactionId, 0, Double.valueOf("0.00"));
@@ -357,6 +375,10 @@ public class LoanTransactionChargebackTest {
assertNotNull(getLoansLoanIdResponse);
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.overpaid");
delinquencyRange = getLoansLoanIdResponse.getDelinquencyRange();
assertNull(delinquencyRange);
log.info("Loan Delinquency Range is null {}", (delinquencyRange == null));
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("0.00"));
}
@@ -383,38 +405,56 @@ public class LoanTransactionChargebackTest {
// First round, empty array
reviewLoanTransactionRelations(loanId, transactionId, 0, Double.valueOf("0.00"));
applyChargebackTransaction(loanId, transactionId, "200.00", 0, responseSpec);
loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "200.00", 0, responseSpec);
Double expectedAmount = Double.valueOf("200.00");
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("200.00"));
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, expectedAmount);
evaluateLoanSummaryAdjustments(getLoansLoanIdResponse, Double.valueOf("200.00"));
loanTransactionHelper.evaluateLoanSummaryAdjustments(getLoansLoanIdResponse, expectedAmount);
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
DelinquencyBucketsHelper.evaluateLoanCollectionData(getLoansLoanIdResponse, 0, expectedAmount);
// Second round, array size equal to 1
reviewLoanTransactionRelations(loanId, transactionId, 1, Double.valueOf("0.00"));
applyChargebackTransaction(loanId, transactionId, "300.00", 1, responseSpec);
loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "300.00", 1, responseSpec);
expectedAmount = Double.valueOf("500.00");
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("500.00"));
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, expectedAmount);
evaluateLoanSummaryAdjustments(getLoansLoanIdResponse, Double.valueOf("500.00"));
loanTransactionHelper.evaluateLoanSummaryAdjustments(getLoansLoanIdResponse, expectedAmount);
DelinquencyBucketsHelper.evaluateLoanCollectionData(getLoansLoanIdResponse, 0, expectedAmount);
// Third round, array size equal to 2
reviewLoanTransactionRelations(loanId, transactionId, 2, Double.valueOf("0.00"));
applyChargebackTransaction(loanId, transactionId, "500.00", 0, responseSpec);
loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "500.00", 0, responseSpec);
expectedAmount = Double.valueOf("1000.00");
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, Double.valueOf("1000.00"));
loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, expectedAmount);
evaluateLoanSummaryAdjustments(getLoansLoanIdResponse, Double.valueOf("1000.00"));
loanTransactionHelper.evaluateLoanSummaryAdjustments(getLoansLoanIdResponse, expectedAmount);
loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
DelinquencyBucketsHelper.evaluateLoanCollectionData(getLoansLoanIdResponse, 0, expectedAmount);
}
private Integer createAccounts(final Integer daysToSubtract, final Integer numberOfRepayments) {
// Delinquency Bucket
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec,
delinquencyBucketId);
// Client and Loan account creation
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012");
final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, null);
final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper,
delinquencyBucketId);
assertNotNull(getLoanProductsProductResponse);
log.info("Loan Product Bucket Name: {}", getLoanProductsProductResponse.getDelinquencyBucket().getName());
assertEquals(getLoanProductsProductResponse.getDelinquencyBucket().getName(), delinquencyBucket.getName());
// Older date to have more than one overdue installment
final LocalDate transactionDate = this.todaysDate.minusDays(daysToSubtract + (30 * (numberOfRepayments - 1)));
@@ -424,16 +464,6 @@ public class LoanTransactionChargebackTest {
operationDate, amountVal, numberOfRepayments.toString());
}
private String createChargebackPayload(final String transactionAmount, final Integer paymentTypeId) {
final HashMap<String, Object> map = new HashMap<>();
map.put("transactionAmount", transactionAmount);
map.put("paymentTypeId", paymentTypeId);
map.put("locale", CommonConstants.LOCALE);
final String chargebackPayload = new Gson().toJson(map);
log.info("{}", chargebackPayload);
return chargebackPayload;
}
private GetLoanProductsProductIdResponse createLoanProduct(final LoanTransactionHelper loanTransactionHelper,
final Integer delinquencyBucketId) {
final HashMap<String, Object> loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucketId);
@@ -457,21 +487,6 @@ public class LoanTransactionChargebackTest {
return loanId;
}
private Integer applyChargebackTransaction(final Integer loanId, final Integer transactionId, final String amount,
final Integer paymentTypeIdx, ResponseSpecification responseSpec) {
List<GetPaymentTypesResponse> paymentTypeList = PaymentTypeHelper.getSystemPaymentType(this.requestSpec, this.responseSpec);
assertTrue(!paymentTypeList.isEmpty());
final String payload = createChargebackPayload(amount, paymentTypeList.get(paymentTypeIdx).getId());
log.info("Loan Chargeback: {}", payload);
PostLoansLoanIdTransactionsTransactionIdResponse postLoansTransactionCommandResponse = loanTransactionHelper
.applyLoanTransactionCommand(loanId, transactionId, "chargeback", payload, responseSpec);
assertNotNull(postLoansTransactionCommandResponse);
log.info("Loan Chargeback Id: {}", postLoansTransactionCommandResponse.getResourceId());
return postLoansTransactionCommandResponse.getResourceId();
}
private void reviewLoanTransactionRelations(final Integer loanId, final Integer transactionId, final Integer expectedSize,
final Double outstandingBalance) {
log.info("Loan Transaction Id: {} {}", loanId, transactionId);
@@ -487,13 +502,4 @@ public class LoanTransactionChargebackTest {
assertEquals(outstandingBalance, getLoansTransactionResponse.getOutstandingLoanBalance());
}
private void evaluateLoanSummaryAdjustments(GetLoansLoanIdResponse getLoansLoanIdResponse, Double amountExpected) {
// Evaluate The Loan Summary Principal Adjustments
GetLoansLoanIdSummary getLoansLoanIdSummary = getLoansLoanIdResponse.getSummary();
if (getLoansLoanIdSummary != null) {
log.info("Loan with Principal Adjustments {} expected {}", getLoansLoanIdSummary.getPrincipalAdjustments(), amountExpected);
assertEquals(amountExpected, getLoansLoanIdSummary.getPrincipalAdjustments());
}
}
}
@@ -20,6 +20,7 @@ package org.apache.fineract.integrationtests.common.loans;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.google.common.reflect.TypeToken;
@@ -43,11 +44,13 @@ 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.GetLoansLoanIdCollectionData;
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.GetLoansLoanIdSummary;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.GetPaymentTypesResponse;
import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
@@ -55,6 +58,7 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionI
import org.apache.fineract.client.models.PutLoansLoanIdResponse;
import org.apache.fineract.client.util.JSON;
import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.PaymentTypeHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Workbook;
@@ -1155,12 +1159,19 @@ public class LoanTransactionHelper {
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());
log.info("Period number {} for due date {} and outstanding {} {}", period.getPeriod(), period.getDueDate(),
period.getTotalOutstandingForPeriod(), period.getComplete());
}
}
}
public void printDelinquencyData(GetLoansLoanIdResponse getLoansLoanIdResponse) {
GetLoansLoanIdCollectionData getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
if (getLoansLoanIdCollectionData != null) {
log.info("Loan Delinquency {}", getLoansLoanIdCollectionData.toString());
}
}
public void validateLoanStatus(GetLoansLoanIdResponse getLoansLoanIdResponse, final String statusCodeExpected) {
final String statusCode = getLoansLoanIdResponse.getStatus().getCode();
log.info("Loan with Id {} is with Status {}", getLoansLoanIdResponse.getId(), statusCode);
@@ -1192,4 +1203,46 @@ public class LoanTransactionHelper {
}
}
public Integer applyChargebackTransaction(final Integer loanId, final Integer transactionId, final String amount,
final Integer paymentTypeIdx, ResponseSpecification responseSpec) {
List<GetPaymentTypesResponse> paymentTypeList = PaymentTypeHelper.getSystemPaymentType(this.requestSpec, this.responseSpec);
assertTrue(!paymentTypeList.isEmpty());
final String payload = createChargebackPayload(amount, paymentTypeList.get(paymentTypeIdx).getId());
log.info("Loan Chargeback: {}", payload);
PostLoansLoanIdTransactionsTransactionIdResponse postLoansTransactionCommandResponse = applyLoanTransactionCommand(loanId,
transactionId, "chargeback", payload, responseSpec);
assertNotNull(postLoansTransactionCommandResponse);
log.info("Loan Chargeback Id: {}", postLoansTransactionCommandResponse.getResourceId());
return postLoansTransactionCommandResponse.getResourceId();
}
public void reviewLoanTransactionRelations(final Integer loanId, final Integer transactionId, final Integer expectedSize) {
GetLoansLoanIdTransactionsTransactionIdResponse getLoansTransactionResponse = getLoanTransaction(loanId, transactionId);
assertNotNull(getLoansTransactionResponse);
assertNotNull(getLoansTransactionResponse.getTransactionRelations());
assertEquals(expectedSize, getLoansTransactionResponse.getTransactionRelations().size());
log.info("Loan with {} Chargeback Transactions", getLoansTransactionResponse.getTransactionRelations().size());
}
public void evaluateLoanSummaryAdjustments(GetLoansLoanIdResponse getLoansLoanIdResponse, Double amountExpected) {
// Evaluate The Loan Summary Principal Adjustments
GetLoansLoanIdSummary getLoansLoanIdSummary = getLoansLoanIdResponse.getSummary();
if (getLoansLoanIdSummary != null) {
log.info("Loan with Principal Adjustments {} expected {}", getLoansLoanIdSummary.getPrincipalAdjustments(), amountExpected);
assertEquals(amountExpected, getLoansLoanIdSummary.getPrincipalAdjustments());
}
}
private String createChargebackPayload(final String transactionAmount, final Integer paymentTypeId) {
final HashMap<String, Object> map = new HashMap<>();
map.put("transactionAmount", transactionAmount);
map.put("paymentTypeId", paymentTypeId);
map.put("locale", CommonConstants.LOCALE);
final String chargebackPayload = new Gson().toJson(map);
log.info("{}", chargebackPayload);
return chargebackPayload;
}
}
@@ -18,6 +18,10 @@
*/
package org.apache.fineract.integrationtests.common.products;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
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;
@@ -25,22 +29,24 @@ import io.restassured.specification.ResponseSpecification;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.DeleteDelinquencyBucketResponse;
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
import org.apache.fineract.client.models.GetDelinquencyRangesResponse;
import org.apache.fineract.client.models.GetLoansLoanIdCollectionData;
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.PutDelinquencyBucketResponse;
import org.apache.fineract.client.util.JSON;
import org.apache.fineract.integrationtests.common.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Slf4j
public class DelinquencyBucketsHelper {
private static final String DELINQUENCY_BUCKETS_URL = "/fineract-provider/api/v1/delinquency/buckets";
private static final Gson GSON = new JSON().getGson();
private static final Logger LOG = LoggerFactory.getLogger(DelinquencyBucketsHelper.class);
protected DelinquencyBucketsHelper() {}
public static ArrayList<GetDelinquencyBucketsResponse> getDelinquencyBuckets(final RequestSpecification requestSpec,
@@ -60,7 +66,7 @@ public class DelinquencyBucketsHelper {
public static PostDelinquencyBucketResponse createDelinquencyBucket(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec, final String json) {
LOG.info("JSON: {}", json);
log.info("JSON: {}", json);
final String response = Utils.performServerPost(requestSpec, responseSpec, DELINQUENCY_BUCKETS_URL + "?" + Utils.TENANT_IDENTIFIER,
json, null);
return GSON.fromJson(response, PostDelinquencyBucketResponse.class);
@@ -68,7 +74,7 @@ public class DelinquencyBucketsHelper {
public static PutDelinquencyBucketResponse updateDelinquencyBucket(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec, final Integer resourceId, final String json) {
LOG.info("JSON: {}", json);
log.info("JSON: {}", json);
final String response = Utils.performServerPut(requestSpec, responseSpec,
DELINQUENCY_BUCKETS_URL + "/" + resourceId + "?" + Utils.TENANT_IDENTIFIER, json, null);
return GSON.fromJson(response, PutDelinquencyBucketResponse.class);
@@ -88,4 +94,52 @@ public class DelinquencyBucketsHelper {
return new Gson().toJson(map);
}
public static Integer createDelinquencyBucket(final RequestSpecification requestSpec, final ResponseSpecification 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());
String jsonBucket = DelinquencyBucketsHelper.getAsJSON(rangeIds);
PostDelinquencyBucketResponse delinquencyBucketResponse = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec,
responseSpec, jsonBucket);
assertNotNull(delinquencyBucketResponse);
return delinquencyBucketResponse.getResourceId();
}
public static void evaluateLoanCollectionData(GetLoansLoanIdResponse getLoansLoanIdResponse, Integer pastDueDays,
Double amountExpected) {
GetLoansLoanIdCollectionData getCollectionData = getLoansLoanIdResponse.getDelinquent();
if (getCollectionData != null) {
log.info("Loan Delinquency Data in Days {} and Amount {}", getCollectionData.getPastDueDays(),
getCollectionData.getDelinquentAmount());
assertEquals(pastDueDays, getCollectionData.getPastDueDays());
assertEquals(amountExpected, getCollectionData.getDelinquentAmount());
} else {
log.info("Loan Delinquency Data is null");
}
GetDelinquencyRangesResponse delinquencyRange = getLoansLoanIdResponse.getDelinquencyRange();
if (delinquencyRange != null) {
log.info("Loan Delinquency Classification is {} : ({} - {}) {}", delinquencyRange.getClassification(),
delinquencyRange.getMinimumAgeDays(), delinquencyRange.getMaximumAgeDays(), pastDueDays);
assertTrue(delinquencyRange.getMinimumAgeDays() <= pastDueDays);
assertTrue(delinquencyRange.getMaximumAgeDays() >= pastDueDays);
} else {
log.info("Loan Delinquency Classification is null");
}
}
}
@@ -85,7 +85,7 @@ public class DelinquencyRangesHelper {
public static String getAsJSON(int minimumAgeDays, int maximumAgeDays) {
final HashMap<String, Object> map = new HashMap<>();
map.put("classification", Utils.randomNameGenerator("Delinquency_", 4));
map.put("classification", Utils.randomNameGenerator("Delinquency__" + minimumAgeDays + "_" + maximumAgeDays + "__", 4));
map.put("minimumAgeDays", minimumAgeDays);
map.put("maximumAgeDays", maximumAgeDays);
map.put("locale", "en");