Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright 2020 The RoboZonky Project
*
* Licensed 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 com.github.robozonky.app.delinquencies;
import static com.github.robozonky.app.events.impl.EventFactory.loanNoLongerDelinquent;
import static com.github.robozonky.app.events.impl.EventFactory.loanNoLongerDelinquentLazy;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.github.robozonky.api.notifications.LoanDefaultedEvent;
import com.github.robozonky.api.notifications.LoanDelinquent10DaysOrMoreEvent;
import com.github.robozonky.api.notifications.LoanDelinquent30DaysOrMoreEvent;
import com.github.robozonky.api.notifications.LoanDelinquent60DaysOrMoreEvent;
import com.github.robozonky.api.notifications.LoanDelinquent90DaysOrMoreEvent;
import com.github.robozonky.api.notifications.LoanNoLongerDelinquentEvent;
import com.github.robozonky.api.notifications.LoanNowDelinquentEvent;
import com.github.robozonky.api.remote.entities.Investment;
import com.github.robozonky.api.remote.entities.Loan;
import com.github.robozonky.api.remote.enums.Label;
import com.github.robozonky.app.events.Events;
import com.github.robozonky.app.events.SessionEvents;
import com.github.robozonky.app.tenant.PowerTenant;
import com.github.robozonky.internal.jobs.TenantPayload;
import com.github.robozonky.internal.remote.Zonky;
import com.github.robozonky.internal.tenant.Tenant;
/**
* Updates delinquency information based on the information about loans that are either currently delinquent or no
* longer active. Will fire events on new delinquencies, defaults and/or loans no longer delinquent.
*/
final class DelinquencyNotificationPayload implements TenantPayload {
private static final Logger LOGGER = LogManager.getLogger(DelinquencyNotificationPayload.class);
private final Function registryFunction;
private final boolean force;
public DelinquencyNotificationPayload() {
this(Registry::new, false);
}
DelinquencyNotificationPayload(final Function registryFunction, final boolean force) {
this.registryFunction = registryFunction;
this.force = force;
}
private static boolean isDefaulted(final Investment i) {
return i.getLoan()
.getLabel()
.map(l -> l == Label.TERMINATED)
.orElse(false);
}
private static void processNoLongerDelinquent(final Registry registry, final Investment investment,
final PowerTenant tenant) {
registry.remove(investment);
LOGGER.debug("Investment identified as no longer delinquent: {}.", investment);
if (investment.getLoan()
.getPayments()
.getUnpaid() == 0 &&
investment.getPrincipal()
.getUnpaid()
.isZero()) {
LOGGER.debug("Ignoring a repaid investment #{}, will be handled elsewhere.",
investment.getId());
return;
}
// TODO Try to convince Zonky to add a dedicated status for loans that are lost.
tenant.fire(loanNoLongerDelinquentLazy(() -> {
final Loan loan = tenant.getLoan(investment.getLoan()
.getId());
return loanNoLongerDelinquent(investment, loan);
}));
}
private static void processDelinquent(final PowerTenant tenant, final Registry registry,
final Investment delinquent) {
final long investmentId = delinquent.getId();
final EnumSet knownCategories = registry.getCategories(delinquent);
if (knownCategories.contains(Category.HOPELESS)) {
LOGGER.debug("Investment #{} may not be promoted anymore.", investmentId);
return;
}
final int daysPastDue = delinquent.getLoan()
.getDpd();
final EnumSet unusedCategories = EnumSet.complementOf(knownCategories);
final Optional firstNextCategory = unusedCategories.stream()
.filter(c -> c.getThresholdInDays() >= 0) // ignore the DEFAULTED category, which gets special treatment
.filter(c -> c.getThresholdInDays() <= daysPastDue)
.max(Comparator.comparing(Category::getThresholdInDays));
if (firstNextCategory.isPresent()) {
final Category category = firstNextCategory.get();
LOGGER.debug("Investment #{} placed to category {}.", investmentId, category);
category.process(tenant, delinquent);
registry.addCategory(delinquent, category);
} else {
LOGGER.debug("Investment #{} can not yet be promoted to the next category.", investmentId);
}
}
private static void processDefaulted(final PowerTenant tenant, final Registry registry,
final Investment currentDelinquent) {
final long investmentId = currentDelinquent.getId();
final EnumSet knownCategories = registry.getCategories(currentDelinquent);
if (knownCategories.contains(Category.DEFAULTED)) {
LOGGER.debug("Investment #{} already tracked as defaulted.", investmentId);
} else {
final Category category = Category.DEFAULTED;
LOGGER.debug("Investment #{} defaulted.", investmentId);
category.process(tenant, currentDelinquent);
registry.addCategory(currentDelinquent, category);
}
}
private static Stream getDefaulted(final Set investments) {
return investments.stream()
.filter(DelinquencyNotificationPayload::isDefaulted);
}
private static Stream getNonDefaulted(final Set investments) {
return investments.stream()
.filter(i -> !isDefaulted(i));
}
private void process(final PowerTenant tenant) {
var delinquents = tenant.call(Zonky::getDelinquentInvestments)
.parallel() // possibly many pages' worth of results; fetch in parallel
.collect(Collectors.toSet());
var count = delinquents.size();
LOGGER.debug("There are {} delinquent investments to process.", count);
var registry = registryFunction.apply(tenant);
if (registry.isInitialized()) {
registry.complement(delinquents)
.parallelStream()
.forEach(i -> {
registry.remove(i);
processNoLongerDelinquent(registry, i, tenant);
});
// potentially thousands of items, with relatively heavy logic behind them
getDefaulted(delinquents).forEach(d -> processDefaulted(tenant, registry, d));
getNonDefaulted(delinquents).forEach(d -> processDelinquent(tenant, registry, d));
} else {
getDefaulted(delinquents).forEach(d -> registry.addCategory(d, Category.DEFAULTED));
getNonDefaulted(delinquents).forEach(delinquent -> {
var dpd = delinquent.getLoan()
.getDpd();
for (var category : Category.values()) {
if (category.getThresholdInDays() > dpd || category.getThresholdInDays() < 0) {
continue;
}
registry.addCategory(delinquent, category);
}
LOGGER.debug("No category found for investment #{}.", delinquent.getId());
});
}
registry.persist();
}
@Override
public void accept(final Tenant tenant) {
PowerTenant powerTenant = (PowerTenant) tenant;
SessionEvents sessionEvents = Events.forSession(powerTenant);
boolean shouldTrigger = force || Stream.of(LoanDefaultedEvent.class, LoanDelinquent10DaysOrMoreEvent.class,
LoanDelinquent30DaysOrMoreEvent.class, LoanDelinquent60DaysOrMoreEvent.class,
LoanDelinquent90DaysOrMoreEvent.class, LoanNowDelinquentEvent.class,
LoanNoLongerDelinquentEvent.class)
.anyMatch(sessionEvents::isListenerRegistered);
if (!shouldTrigger) {
LOGGER.debug("Skipping on account of no event listener being configured to receive the results.");
return;
}
powerTenant.inTransaction(this::process);
}
}