blob: 29451c169dd2ab784d8d72f4ec58141ec91afb94 [file] [log] [blame]
/**
* 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.delinquency.service;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
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.infrastructure.event.business.domain.loan.LoanAccountDelinquencyPauseChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanDelinquencyRangeChangeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiConstants;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappings;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappingsRepository;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository;
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
import org.apache.fineract.portfolio.delinquency.exception.DelinquencyBucketAgesOverlapedException;
import org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInvalidAgesException;
import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParseAndValidator;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
import org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Slf4j
public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlatformService {
private final DelinquencyBucketParseAndValidator dataValidatorBucket;
private final DelinquencyRangeParseAndValidator dataValidatorRange;
private final DelinquencyRangeRepository repositoryRange;
private final DelinquencyBucketRepository repositoryBucket;
private final DelinquencyBucketMappingsRepository repositoryBucketMappings;
private final LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository;
private final LoanRepositoryWrapper loanRepository;
private final LoanProductRepository loanProductRepository;
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
private final LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository;
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
private final LoanDelinquencyActionRepository loanDelinquencyActionRepository;
private final DelinquencyActionParseAndValidator delinquencyActionParseAndValidator;
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
private final BusinessEventNotifierService businessEventNotifierService;
private final DelinquencyWritePlatformServiceHelper delinquencyHelper;
@Override
public CommandProcessingResult createDelinquencyRange(JsonCommand command) {
DelinquencyRangeData data = dataValidatorRange.validateAndParseUpdate(command);
Map<String, Object> changes = new HashMap<>();
DelinquencyRange delinquencyRange = createDelinquencyRange(data, changes);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyRange.getId()).with(changes)
.build();
}
@Override
public CommandProcessingResult updateDelinquencyRange(Long delinquencyRangeId, JsonCommand command) {
DelinquencyRangeData data = dataValidatorRange.validateAndParseUpdate(command);
DelinquencyRange delinquencyRange = this.repositoryRange.getReferenceById(delinquencyRangeId);
Map<String, Object> changes = new HashMap<>();
delinquencyRange = updateDelinquencyRange(delinquencyRange, data, changes);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyRange.getId()).with(changes)
.build();
}
@Override
public CommandProcessingResult deleteDelinquencyRange(Long delinquencyRangeId, JsonCommand command) {
final DelinquencyRange delinquencyRange = repositoryRange.getReferenceById(delinquencyRangeId);
if (delinquencyRange != null) {
final Long delinquencyRangeLinked = this.loanDelinquencyTagRepository.countByDelinquencyRange(delinquencyRange);
if (delinquencyRangeLinked > 0) {
throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.linked",
"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(delinquencyRangeId).build();
}
@Override
public CommandProcessingResult createDelinquencyBucket(JsonCommand command) {
DelinquencyBucketData data = dataValidatorBucket.validateAndParseUpdate(command);
Map<String, Object> changes = new HashMap<>();
DelinquencyBucket delinquencyBucket = createDelinquencyBucket(data, changes);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyBucket.getId()).with(changes)
.build();
}
@Override
public CommandProcessingResult updateDelinquencyBucket(Long delinquencyBucketId, JsonCommand command) {
DelinquencyBucketData data = dataValidatorBucket.validateAndParseUpdate(command);
DelinquencyBucket delinquencyBucket = this.repositoryBucket.getReferenceById(delinquencyBucketId);
Map<String, Object> changes = new HashMap<>();
delinquencyBucket = updateDelinquencyBucket(delinquencyBucket, data, changes);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyBucket.getId()).with(changes)
.build();
}
@Override
public CommandProcessingResult deleteDelinquencyBucket(Long delinquencyBucketId, JsonCommand command) {
final DelinquencyBucket delinquencyBucket = repositoryBucket.getReferenceById(delinquencyBucketId);
if (delinquencyBucket != null) {
Long delinquencyBucketLinked = this.loanProductRepository.countByDelinquencyBucket(delinquencyBucket);
if (delinquencyBucketLinked > 0) {
throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.linked",
"Data integrity issue with resource: " + delinquencyBucket.getId());
}
repositoryBucket.delete(delinquencyBucket);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyBucketId).build();
}
@Override
public LoanScheduleDelinquencyData calculateDelinquencyData(LoanScheduleDelinquencyData loanScheduleDelinquencyData,
List<LoanDelinquencyActionData> effectiveDelinquencyList) {
Loan loan = loanScheduleDelinquencyData.getLoan();
if (loan == null) {
loan = this.loanRepository.findOneWithNotFoundDetection(loanScheduleDelinquencyData.getLoanId());
}
final CollectionData collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
log.debug("Delinquency {}", collectionData);
return new LoanScheduleDelinquencyData(loan.getId(), collectionData.getDelinquentDate(), collectionData.getDelinquentDays(), loan);
}
@Override
public CommandProcessingResult applyDelinquencyTagToLoan(Long loanId, JsonCommand command) {
Map<String, Object> changes = new HashMap<>();
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
final List<LoanDelinquencyAction> savedDelinquencyList = delinquencyReadPlatformService
.retrieveLoanDelinquencyActions(loan.getId());
List<LoanDelinquencyActionData> effectiveDelinquencyList = delinquencyEffectivePauseHelper
.calculateEffectiveDelinquencyList(savedDelinquencyList);
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket();
if (delinquencyBucket != null) {
final LoanDelinquencyData loanDelinquencyData = loanDelinquencyDomainService.getLoanDelinquencyData(loan,
effectiveDelinquencyList);
// loan delinquent data
final CollectionData collectionData = loanDelinquencyData.getLoanCollectionData();
// loan installments delinquent data
final Map<Long, CollectionData> installmentsCollectionData = loanDelinquencyData.getLoanInstallmentsCollectionData();
log.debug("Delinquency {}", collectionData);
changes = applyDelinquencyToLoanAndInstallments(loan, delinquencyBucket, collectionData, installmentsCollectionData);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
.withEntityExternalId(loan.getExternalId()) //
.with(changes) //
.build(); //
}
@Override
public void applyDelinquencyTagToLoan(LoanScheduleDelinquencyData loanDelinquencyData,
List<LoanDelinquencyActionData> effectiveDelinquencyList) {
final Loan loan = loanDelinquencyData.getLoan();
if (loan.hasDelinquencyBucket()) {
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket();
final LoanDelinquencyData loanDelinquentData = loanDelinquencyDomainService.getLoanDelinquencyData(loan,
effectiveDelinquencyList);
// loan delinquent data
final CollectionData collectionData = loanDelinquentData.getLoanCollectionData();
// loan installments delinquent data
final Map<Long, CollectionData> installmentsCollectionData = loanDelinquentData.getLoanInstallmentsCollectionData();
log.debug("Delinquency {}", collectionData);
applyDelinquencyToLoanAndInstallments(loan, delinquencyBucket, collectionData, installmentsCollectionData);
}
}
private Map<String, Object> applyDelinquencyToLoanAndInstallments(Loan loan, DelinquencyBucket delinquencyBucket,
CollectionData collectionData, Map<Long, CollectionData> installmentsCollectionData) {
// Order is important: first calculate loan level delinquency, then the installment level
// delinquency for loan
Map<String, Object> result = delinquencyHelper.applyDelinquencyForLoan(loan, delinquencyBucket, collectionData.getDelinquentDays());
// delinquency for installments
if (!installmentsCollectionData.isEmpty()) {
delinquencyHelper.applyDelinquencyForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData);
}
return result;
}
@Override
@Transactional
public CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand command) {
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
final List<LoanDelinquencyAction> savedDelinquencyList = delinquencyReadPlatformService.retrieveLoanDelinquencyActions(loanId);
LoanDelinquencyAction parsedDelinquencyAction = delinquencyActionParseAndValidator.validateAndParseUpdate(command, loan,
savedDelinquencyList, businessDate);
parsedDelinquencyAction.setLoan(loan);
LoanDelinquencyAction saved = loanDelinquencyActionRepository.saveAndFlush(parsedDelinquencyAction);
// if backdated pause recalculate delinquency data
if (DateUtils.isBefore(parsedDelinquencyAction.getStartDate(), businessDate)
&& DelinquencyAction.PAUSE.equals(parsedDelinquencyAction.getAction())) {
recalculateLoanDelinquencyData(loan);
// if pause end date is after current business date, loan delinquency pause flag is changed, emit event
if (DateUtils.isAfter(parsedDelinquencyAction.getEndDate(), businessDate)) {
businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan));
}
}
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountDelinquencyPauseChangedBusinessEvent(loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(saved.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.build();
}
private void recalculateLoanDelinquencyData(Loan loan) {
List<LoanDelinquencyAction> savedDelinquencyList = delinquencyReadPlatformService.retrieveLoanDelinquencyActions(loan.getId());
List<LoanDelinquencyActionData> effectiveDelinquencyList = delinquencyEffectivePauseHelper
.calculateEffectiveDelinquencyList(savedDelinquencyList);
CollectionData loanDelinquencyData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(loan.getId(),
loanDelinquencyData.getDelinquentDate(), loanDelinquencyData.getDelinquentDays(), loan);
if (loanScheduleDelinquencyData.getOverdueDays() > 0) {
applyDelinquencyTagToLoan(loanScheduleDelinquencyData, effectiveDelinquencyList);
} else {
removeDelinquencyTagToLoan(loan);
}
}
@Override
public void removeDelinquencyTagToLoan(final Loan loan) {
if (loan.isEnableInstallmentLevelDelinquency()) {
cleanLoanInstallmentsDelinquencyTags(loan);
}
delinquencyHelper.setLoanDelinquencyTag(loan, null);
}
@Override
public void cleanLoanDelinquencyTags(Loan loan) {
List<LoanDelinquencyTagHistory> loanDelinquencyTags = this.loanDelinquencyTagRepository.findByLoan(loan);
if (loanDelinquencyTags != null && loanDelinquencyTags.size() > 0) {
this.loanDelinquencyTagRepository.deleteAll(loanDelinquencyTags);
}
}
private DelinquencyRange createDelinquencyRange(DelinquencyRangeData data, Map<String, Object> changes) {
Optional<DelinquencyRange> delinquencyRange = repositoryRange.findByClassification(data.getClassification());
if (delinquencyRange.isEmpty()) {
if (data.getMaximumAgeDays() != null && data.getMinimumAgeDays() > data.getMaximumAgeDays()) {
final String errorMessage = "The age days values are invalid, the maximum age days can't be lower than minimum age days";
throw new DelinquencyRangeInvalidAgesException(errorMessage, data.getMinimumAgeDays(), data.getMaximumAgeDays());
}
DelinquencyRange newDelinquencyRange = DelinquencyRange.instance(data.getClassification(), data.getMinimumAgeDays(),
data.getMaximumAgeDays());
return repositoryRange.saveAndFlush(newDelinquencyRange);
} else {
throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.duplicated",
"Data integrity issue with resource: " + delinquencyRange.get().getId());
}
}
private DelinquencyRange updateDelinquencyRange(DelinquencyRange delinquencyRange, DelinquencyRangeData data,
Map<String, Object> changes) {
if (!data.getClassification().equalsIgnoreCase(delinquencyRange.getClassification())) {
delinquencyRange.setClassification(data.getClassification());
changes.put(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, data.getClassification());
}
if (!data.getMinimumAgeDays().equals(delinquencyRange.getMinimumAgeDays())) {
delinquencyRange.setMinimumAgeDays(data.getMinimumAgeDays());
changes.put(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME, data.getMinimumAgeDays());
}
if (!data.getMaximumAgeDays().equals(delinquencyRange.getMaximumAgeDays())) {
delinquencyRange.setMaximumAgeDays(data.getMaximumAgeDays());
changes.put(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, data.getMaximumAgeDays());
}
if (!changes.isEmpty()) {
delinquencyRange = repositoryRange.saveAndFlush(delinquencyRange);
}
return delinquencyRange;
}
private DelinquencyBucket createDelinquencyBucket(DelinquencyBucketData data, Map<String, Object> changes) {
Optional<DelinquencyBucket> delinquencyBucket = repositoryBucket.findByName(data.getName());
if (delinquencyBucket.isEmpty()) {
DelinquencyBucket newDelinquencyBucket = new DelinquencyBucket(data.getName());
repositoryBucket.save(newDelinquencyBucket);
setDelinquencyBucketMappings(newDelinquencyBucket, data);
return newDelinquencyBucket;
} else {
throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.duplicated",
"Data integrity issue with resource: " + delinquencyBucket.get().getId());
}
}
private DelinquencyBucket updateDelinquencyBucket(DelinquencyBucket delinquencyBucket, DelinquencyBucketData data,
Map<String, Object> changes) {
if (!data.getName().equalsIgnoreCase(delinquencyBucket.getName())) {
delinquencyBucket.setName(data.getName());
changes.put(DelinquencyApiConstants.NAME_PARAM_NAME, data.getName());
}
if (!changes.isEmpty()) {
delinquencyBucket = repositoryBucket.save(delinquencyBucket);
}
setDelinquencyBucketMappings(delinquencyBucket, data);
return delinquencyBucket;
}
private void setDelinquencyBucketMappings(DelinquencyBucket delinquencyBucket, DelinquencyBucketData data) {
List<Long> rangeIds = new ArrayList<>();
data.getRanges().forEach(dataRange -> {
rangeIds.add(dataRange.getId());
});
List<DelinquencyRange> ranges = repositoryRange.findAllById(rangeIds);
validateDelinquencyRanges(ranges);
List<DelinquencyBucketMappings> bucketMappings = repositoryBucketMappings.findByDelinquencyBucket(delinquencyBucket);
repositoryBucketMappings.deleteAll(bucketMappings);
bucketMappings.clear();
for (DelinquencyRange delinquencyRange : ranges) {
bucketMappings.add(DelinquencyBucketMappings.instance(delinquencyBucket, delinquencyRange));
}
repositoryBucketMappings.saveAllAndFlush(bucketMappings);
}
private void validateDelinquencyRanges(List<DelinquencyRange> ranges) {
// Sort the ranges based on the minAgeDays
ranges = delinquencyHelper.sortDelinquencyRangesByMinAge(ranges);
DelinquencyRange prevDelinquencyRange = null;
for (DelinquencyRange delinquencyRange : ranges) {
if (prevDelinquencyRange != null) {
if (isOverlapped(prevDelinquencyRange, delinquencyRange)) {
final String errorMessage = "The delinquency ranges age days values are overlaped";
throw new DelinquencyBucketAgesOverlapedException(errorMessage, prevDelinquencyRange, delinquencyRange);
}
}
prevDelinquencyRange = delinquencyRange;
}
}
private boolean isOverlapped(DelinquencyRange o1, DelinquencyRange o2) {
if (o2.getMaximumAgeDays() != null) { // Max Age undefined - Last one
return Math.max(o1.getMinimumAgeDays(), o2.getMinimumAgeDays()) <= Math.min(o1.getMaximumAgeDays(), o2.getMaximumAgeDays());
} else {
return Math.max(o1.getMinimumAgeDays(), o2.getMinimumAgeDays()) <= Math.min(o1.getMaximumAgeDays(), o2.getMinimumAgeDays());
}
}
private void cleanLoanInstallmentsDelinquencyTags(Loan loan) {
loanInstallmentDelinquencyTagRepository.deleteAllLoanInstallmentsTags(loan.getId());
}
}