Skip to content
This repository has been archived by the owner on Oct 31, 2021. It is now read-only.

Commit

Permalink
Skip investments until CAPTCHA is gone (fixes #103)
Browse files Browse the repository at this point in the history
The seemingly unrelated changes in this commit are there to increase mutation coverage in robozonky-app over the threshold.
  • Loading branch information
triceo committed Mar 8, 2017
1 parent 22ea158 commit 818f507
Show file tree
Hide file tree
Showing 14 changed files with 278 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2017 Lukáš Petrovický
*
* 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.api.notifications;

import com.github.triceo.robozonky.api.strategies.Recommendation;

/**
* Fired when an event was skipped by the investment algorithm due to CAPTCHA, to be evaluated later after CAPTCHA
* expires.
*/
public final class InvestmentSkippedEvent extends Event {

private final Recommendation recommendation;

public InvestmentSkippedEvent(final Recommendation recommendation) {
this.recommendation = recommendation;
}

public Recommendation getRecommendation() {
return recommendation;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ public synchronized boolean isSeenBefore(final int loanId) {
return this.seenLoans.contains(loanId);
}

public synchronized boolean isDiscarded(final int loanId) {
return this.discardedLoans.contains(loanId);
}

/**
* Mark a given loan as no longer relevant for this session.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.github.triceo.robozonky.api.notifications.InvestmentMadeEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentRejectedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentRequestedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentSkippedEvent;
import com.github.triceo.robozonky.api.notifications.LoanRecommendedEvent;
import com.github.triceo.robozonky.api.notifications.StrategyCompletedEvent;
import com.github.triceo.robozonky.api.notifications.StrategyStartedEvent;
Expand Down Expand Up @@ -81,9 +82,18 @@ static Optional<Investment> actuallyInvest(final Recommendation recommendation,
final String providerId = api.getConfirmationProviderId().orElse("-");
switch (response.getType()) {
case REJECTED:
Events.fire(new InvestmentRejectedEvent(recommendation, balance.intValue(), providerId));
tracker.discardLoan(loanId);
return Optional.empty();
return api.getConfirmationProviderId().map(c -> {
Events.fire(new InvestmentRejectedEvent(recommendation, balance.intValue(), providerId));
// rejected through a confirmation provider => forget
tracker.discardLoan(loanId);
return Optional.<Investment>empty();
}).orElseGet(() -> {
// rejected due to no confirmation provider => make available for direct investment later
Events.fire(new InvestmentSkippedEvent(recommendation));
Investor.LOGGER.debug("Loan #{} protected by CAPTCHA, will check back later.", loanId);
tracker.ignoreLoan(loanId);
return Optional.empty();
});
case DELEGATED:
Events.fire(new InvestmentDelegatedEvent(recommendation, balance.intValue(), providerId));
if (recommendation.isConfirmationRequired()) {
Expand All @@ -101,9 +111,10 @@ static Optional<Investment> actuallyInvest(final Recommendation recommendation,
tracker.makeInvestment(i);
return Optional.of(i);
case SEEN_BEFORE:
Events.fire(new InvestmentSkippedEvent(recommendation));
return Optional.empty();
default:
throw new IllegalStateException("Not possible. ");
throw new IllegalStateException("Not possible.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ static String findLastStable(final Set<String> versions) {
.orElseThrow(() -> new IllegalStateException("Impossible."));
}

private static VersionIdentifier parseNodeList(final NodeList nodeList) {
static VersionIdentifier parseNodeList(final NodeList nodeList) {
final SortedSet<String> versions = new TreeSet<>(new VersionComparator());
for (int i = 0; i < nodeList.getLength(); i++) {
final String version = nodeList.item(i).getTextContent();
Expand All @@ -103,7 +103,7 @@ private static VersionIdentifier parseNodeList(final NodeList nodeList) {
final String stable = VersionRetriever.findLastStable(versions);
// and check if it is followed by any other versions
final SortedSet<String> tail = versions.tailSet(stable);
if (tail.isEmpty()) {
if (tail.size() == 1) {
return new VersionIdentifier(stable);
} else {
return new VersionIdentifier(stable, tail.last());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.github.triceo.robozonky.api.notifications.InvestmentMadeEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentRejectedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentRequestedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentSkippedEvent;
import com.github.triceo.robozonky.api.remote.entities.Investment;
import com.github.triceo.robozonky.api.strategies.LoanDescriptor;
import com.github.triceo.robozonky.api.strategies.Recommendation;
Expand Down Expand Up @@ -130,6 +131,57 @@ public void investmentDelegated() {
Assertions.assertThat(t2.isSeenBefore(loanId)).isTrue();
}

@Test
public void investmentDelegatedButExpectedConfirmed() {
final LoanDescriptor ld = AbstractInvestingTest.mockLoanDescriptor();
final Collection<LoanDescriptor> availableLoans = Collections.singletonList(ld);
final InvestmentTracker t = new InvestmentTracker(availableLoans, BigDecimal.valueOf(10000));
final Recommendation r = ld.recommend(200, true).get();
final int loanId = ld.getLoan().getId();
final ZonkyProxy api = Mockito.mock(ZonkyProxy.class);
Mockito.when(api.invest(ArgumentMatchers.eq(r), ArgumentMatchers.anyBoolean()))
.thenReturn(new ZonkyResponse(ZonkyResponseType.DELEGATED));
Mockito.when(api.getConfirmationProviderId()).thenReturn(Optional.of("something"));
Assertions.assertThat(t.isDiscarded(loanId)).isFalse();
final Optional<Investment> result = Investor.actuallyInvest(r, api, t);
Assertions.assertThat(t.isDiscarded(loanId)).isTrue();
Assertions.assertThat(result).isEmpty();
// validate event
final List<Event> newEvents = this.getNewEvents();
Assertions.assertThat(newEvents).hasSize(2);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(newEvents.get(0)).isInstanceOf(InvestmentRequestedEvent.class);
softly.assertThat(newEvents.get(1)).isInstanceOf(InvestmentDelegatedEvent.class);
});
// check that discard information is persisted
final InvestmentTracker t2 = new InvestmentTracker(availableLoans, BigDecimal.valueOf(10000));
Assertions.assertThat(t2.isDiscarded(loanId)).isTrue();
}

@Test
public void investmentIgnoredWhenNoConfirmationProviderAndCaptcha() {
final LoanDescriptor ld = AbstractInvestingTest.mockLoanDescriptor();
final Collection<LoanDescriptor> availableLoans = Collections.singletonList(ld);
final InvestmentTracker t = new InvestmentTracker(availableLoans, BigDecimal.valueOf(10000));
final Recommendation r = ld.recommend(200).get();
final int loanId = ld.getLoan().getId();
final ZonkyProxy api = Mockito.mock(ZonkyProxy.class);
Mockito.when(api.invest(ArgumentMatchers.eq(r), ArgumentMatchers.anyBoolean()))
.thenReturn(new ZonkyResponse(ZonkyResponseType.REJECTED));
Mockito.when(api.getConfirmationProviderId()).thenReturn(Optional.empty());
Assertions.assertThat(t.isSeenBefore(loanId)).isFalse();
final Optional<Investment> result = Investor.actuallyInvest(r, api, t);
Assertions.assertThat(t.isSeenBefore(loanId)).isTrue();
Assertions.assertThat(result).isEmpty();
// validate event
final List<Event> newEvents = this.getNewEvents();
Assertions.assertThat(newEvents).hasSize(2);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(newEvents.get(0)).isInstanceOf(InvestmentRequestedEvent.class);
softly.assertThat(newEvents.get(1)).isInstanceOf(InvestmentSkippedEvent.class);
});
}

@Test
public void investmentSuccessful() {
final BigDecimal oldBalance = BigDecimal.valueOf(10000);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
import java.util.Collections;

import org.assertj.core.api.Assertions;
import org.assertj.core.api.SoftAssertions;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class VersionRetrieverTest {

Expand All @@ -43,4 +48,37 @@ public void checkNoStable() {
.isInstanceOf(IllegalStateException.class);
}

@Test
public void parseSingleNodeList() {
final String version = "1.2.3";
final Node n = Mockito.mock(Node.class);
Mockito.when(n.getTextContent()).thenReturn(version);
final NodeList l = Mockito.mock(NodeList.class);
Mockito.when(l.getLength()).thenReturn(1);
Mockito.when(l.item(ArgumentMatchers.eq(0))).thenReturn(n);
final VersionIdentifier actual = VersionRetriever.parseNodeList(l);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(actual.getLatestStable()).isEqualTo(version);
softly.assertThat(actual.getLatestUnstable()).isEmpty();
});
}

@Test
public void parseLongerNodeList() {
final String version = "1.2.3", version2 = "1.2.4-SNAPSHOT";
final Node n1 = Mockito.mock(Node.class);
Mockito.when(n1.getTextContent()).thenReturn(version);
final Node n2 = Mockito.mock(Node.class);
Mockito.when(n2.getTextContent()).thenReturn(version2);
final NodeList l = Mockito.mock(NodeList.class);
Mockito.when(l.getLength()).thenReturn(2);
Mockito.when(l.item(ArgumentMatchers.eq(0))).thenReturn(n1);
Mockito.when(l.item(ArgumentMatchers.eq(1))).thenReturn(n2);
final VersionIdentifier actual = VersionRetriever.parseNodeList(l);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(actual.getLatestStable()).isEqualTo(version);
softly.assertThat(actual.getLatestUnstable()).contains(version2);
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ public static Properties configureEmailNotifications(final InstallData data) {
p.setProperty("smtp.port", toInt(Variables.SMTP_PORT.getValue(data)));
p.setProperty("smtp.requiresStartTLS", toBoolean(Variables.SMTP_IS_TLS.getValue(data)));
p.setProperty("smtp.requiresSslOnConnect", toBoolean(Variables.SMTP_IS_SSL.getValue(data)));
p.setProperty("investmentRejected.enabled", toBoolean(Variables.EMAIL_IS_INVESTMENT.getValue(data)));
p.setProperty("investmentMade.enabled", toBoolean(Variables.EMAIL_IS_INVESTMENT.getValue(data)));
p.setProperty("investmentDelegated.enabled", toBoolean(Variables.EMAIL_IS_INVESTMENT.getValue(data)));
final String isInvestmentEmailEnabled = toBoolean(Variables.EMAIL_IS_INVESTMENT.getValue(data));
p.setProperty("investmentSkipped.enabled", isInvestmentEmailEnabled);
p.setProperty("investmentRejected.enabled", isInvestmentEmailEnabled);
p.setProperty("investmentMade.enabled", isInvestmentEmailEnabled);
p.setProperty("investmentDelegated.enabled", isInvestmentEmailEnabled);
p.setProperty("balanceTracker.enabled", toBoolean(Variables.EMAIL_IS_BALANCE_OVER_200.getValue(data)));
p.setProperty("balanceTracker.targetBalance", "200");
p.setProperty("roboZonkyDaemonFailed.enabled", toBoolean(Variables.EMAIL_IS_FAILURE.getValue(data)));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2017 Lukáš Petrovický
*
* 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.notifications.email;

import java.util.HashMap;
import java.util.Map;

import com.github.triceo.robozonky.api.notifications.InvestmentSkippedEvent;
import com.github.triceo.robozonky.api.remote.entities.Loan;

final class InvestmentSkippedEventListener extends AbstractEmailingListener<InvestmentSkippedEvent> {

public InvestmentSkippedEventListener(final ListenerSpecificNotificationProperties properties) {
super(properties);
}

@Override
String getSubject(final InvestmentSkippedEvent event) {
return "Půjčka č. " + event.getRecommendation().getLoanDescriptor().getLoan().getId() + " dočasně přeskočena";
}

@Override
String getTemplateFileName() {
return "investment-skipped.ftl";
}

@Override
Map<String, Object> getData(final InvestmentSkippedEvent event) {
final Loan loan = event.getRecommendation().getLoanDescriptor().getLoan();
final Map<String, Object> result = new HashMap<>();
result.put("loanId", loan.getId());
result.put("loanRecommendation", event.getRecommendation().getRecommendedInvestmentAmount());
result.put("loanAmount", loan.getAmount());
result.put("loanRating", loan.getRating().getCode());
result.put("loanTerm", loan.getTermInMonths());
return result;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.github.triceo.robozonky.api.notifications.InvestmentDelegatedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentMadeEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentRejectedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentSkippedEvent;
import com.github.triceo.robozonky.api.notifications.RemoteOperationFailedEvent;
import com.github.triceo.robozonky.api.notifications.RoboZonkyCrashedEvent;
import com.github.triceo.robozonky.api.notifications.RoboZonkyDaemonFailedEvent;
Expand All @@ -46,6 +47,21 @@ public Class<? extends Event> getEventType() {
protected EventListener<? extends Event> newListener(final ListenerSpecificNotificationProperties properties) {
return new InvestmentMadeEventListener(properties);
}
}, INVESTMENT_SKIPPED {
@Override
public String getId() {
return "investmentSkipped";
}

@Override
public Class<? extends Event> getEventType() {
return InvestmentSkippedEvent.class;
}

@Override
protected EventListener<? extends Event> newListener(final ListenerSpecificNotificationProperties properties) {
return new InvestmentSkippedEventListener(properties);
}
}, INVESTMENT_DELEGATED {
@Override
public String getId() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.github.triceo.robozonky.api.notifications.InvestmentDelegatedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentMadeEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentRejectedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentSkippedEvent;
import com.github.triceo.robozonky.api.notifications.ListenerService;

public final class FileStoringListenerService implements ListenerService {
Expand All @@ -36,6 +37,8 @@ public <T extends Event> Refreshable<EventListener<T>> findListener(final Class<
return Refreshable.createImmutable((EventListener<T>) new InvestmentRejectedEventListener());
} else if (Objects.equals(eventType, InvestmentDelegatedEvent.class)) {
return Refreshable.createImmutable((EventListener<T>) new InvestmentDelegatedEventListener());
} else if (Objects.equals(eventType, InvestmentSkippedEvent.class)) {
return Refreshable.createImmutable((EventListener<T>) new InvestmentSkippedEventListener());
}
return Refreshable.createImmutable(null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2017 Lukáš Petrovický
*
* 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.notifications.files;

import com.github.triceo.robozonky.api.notifications.InvestmentSkippedEvent;

final class InvestmentSkippedEventListener extends AbstractFileStoringListener<InvestmentSkippedEvent> {

@Override
int getLoanId(final InvestmentSkippedEvent event) {
return event.getRecommendation().getLoanDescriptor().getLoan().getId();
}

@Override
int getAmount(final InvestmentSkippedEvent event) {
return event.getRecommendation().getRecommendedInvestmentAmount();
}

@Override
String getSuffix() {
return "skipped";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Půjčka s následujícími parametry byla přeskočena:

- Číslo půjčky: ${data.loanId?c}
- Rating: ${data.loanRating}
- Délka splácení: ${data.loanTerm?c} měsíců
- Požadovaná částka: ${data.loanAmount?c},- Kč
- Navržená výše investice: ${data.loanRecommendation?c},- Kč

Informace o této půjčce jsou dostupné na následující adrese:
https://app.zonky.cz/#/marketplace/detail/${data.loanId?c}/

Pokud tato půjčka vydrží na tržišti až do vypršení CAPTCHA, robot se k ní vrátí.

Loading

0 comments on commit 818f507

Please sign in to comment.