
com.github.triceo.robozonky.app.portfolio.Delinquents Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2017 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.triceo.robozonky.app.portfolio;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.github.triceo.robozonky.api.notifications.LoanNoLongerDelinquentEvent;
import com.github.triceo.robozonky.api.remote.entities.Investment;
import com.github.triceo.robozonky.api.remote.enums.PaymentStatus;
import com.github.triceo.robozonky.app.Events;
import com.github.triceo.robozonky.common.remote.Zonky;
import com.github.triceo.robozonky.internal.api.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Main entry point to the delinquency API.
*/
public enum Delinquents {
INSTANCE; // cheap thread-safe singleton
private static final Logger LOGGER = LoggerFactory.getLogger(Delinquents.class);
private static final String TIME_SEPARATOR = ":::";
private static final Pattern TIME_SPLITTER = Pattern.compile("\\Q" + TIME_SEPARATOR + "\\E");
private static String toString(final Delinquency d) {
return d.getFixedOn()
.map(fixedOn -> d.getDetectedOn() + TIME_SEPARATOR + fixedOn)
.orElse(d.getDetectedOn().toString());
}
private static Stream toString(final Delinquent d) {
return d.getDelinquencies().map(Delinquents::toString);
}
private static void add(final Delinquent d, final String delinquency) {
final String[] parts = TIME_SPLITTER.split(delinquency);
if (parts.length == 1) {
d.addDelinquency(LocalDate.parse(parts[0]));
} else if (parts.length == 2) {
d.addDelinquency(LocalDate.parse(parts[0]), LocalDate.parse(parts[1]));
} else {
throw new IllegalStateException("Unexpected number of dates: " + parts.length);
}
}
private static Delinquent add(final int loanId, final List delinquencies) {
final Delinquent d = new Delinquent(loanId);
delinquencies.forEach(delinquency -> add(d, delinquency));
return d;
}
public void update(final Zonky zonky, final Collection presentlyDelinquent) {
update(zonky, presentlyDelinquent, Collections.emptyList());
}
/**
* Updates delinquency information based on the information about loans that are either currently delinquent or no
* longer active. Will fire events on new delinquencies and/or on loans no longer delinquent.
* @param zonky The API that will be used to retrieve the loan instances.
* @param presentlyDelinquent Loans that currently have overdue instalments. This corresponds to
* {@link PaymentStatus#getDelinquent()}
* @param noLongerActive Loans that are no longer relevant. This corresponds to {@link PaymentStatus#getDone()}.
*/
public void update(final Zonky zonky, final Collection presentlyDelinquent,
final Collection noLongerActive) {
LOGGER.debug("Updating delinquent loans.");
final LocalDate now = LocalDate.now();
final Collection knownDelinquents = this.getDelinquents();
knownDelinquents.stream()
.filter(Delinquent::hasActiveDelinquency) // only care about present delinquents
.filter(d -> presentlyDelinquent.stream().noneMatch(i -> d.getLoanId() == i.getLoanId()))
.flatMap(d -> d.getActiveDelinquency().map(Stream::of).orElse(Stream.empty()))
.peek(d -> d.setFixedOn(now.minusDays(1))) // end the delinquency
.map(Delinquency::getParent)
.forEach(d -> Events.fire(new LoanNoLongerDelinquentEvent(d.getLoan(zonky)))); // notify
final Stream stillDelinquent = knownDelinquents.stream()
.filter(d -> noLongerActive.stream().noneMatch(i -> d.getLoanId() == i.getLoanId()));
final Stream newDelinquents = presentlyDelinquent.stream()
.filter(i -> knownDelinquents.stream().noneMatch(d -> d.getLoanId() == i.getLoanId()))
.map(i -> {
// no matter when we register delinquency, we can be sure it's delinquent from the last payment day
final LocalDate delinquentPayment = (i.getNextPaymentDate() == null) ? now :
i.getNextPaymentDate().toLocalDate().minusMonths(1).plusDays(1);
return new Delinquent(i.getLoanId(), delinquentPayment);
});
synchronized (this) { // store to the state file
LOGGER.trace("Starting delinquency update.");
final State.ClassSpecificState state = State.forClass(this.getClass());
final State.Batch stateUpdate = state.newBatch(true);
// update state of delinquents
final Collection allPresent = Stream.concat(stillDelinquent, newDelinquents)
.peek(d -> stateUpdate.set(String.valueOf(d.getLoanId()), toString(d)))
.flatMap(d -> d.getActiveDelinquency().map(Stream::of).orElse(Stream.empty()))
.collect(Collectors.toSet());
stateUpdate.call(); // persist state updates
LOGGER.trace("Delinquency update finished.");
// and notify of new delinquencies over all known thresholds
Stream.of(DelinquencyCategory.values()).forEach(c -> c.update(allPresent, zonky));
}
LOGGER.trace("Done.");
}
/**
* @return Active loans that are now, or at some point have been, currently tracked as delinquent.
*/
public Collection getDelinquents() {
final State.ClassSpecificState state = State.forClass(this.getClass());
return state.getKeys().stream()
.map(key -> {
final int loanId = Integer.parseInt(key);
final List rawDelinquencies =
state.getValues(key).orElseThrow(() -> new IllegalStateException("Impossible."));
return add(loanId, rawDelinquencies);
}).collect(Collectors.toSet());
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy