refactored/tested
This commit is contained in:
parent
c987bc725f
commit
7ac5c69eff
3
buxfer-cli/.gitignore
vendored
Normal file
3
buxfer-cli/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Personal
|
||||
buxfer-personal.properties
|
||||
|
110
buxfer-cli/pom.xml
Normal file
110
buxfer-cli/pom.xml
Normal file
@ -0,0 +1,110 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>buxfer-cli</artifactId>
|
||||
<name>Buxfer CLI</name>
|
||||
|
||||
<parent>
|
||||
<groupId>com.inteligr8.buxfer</groupId>
|
||||
<artifactId>buxfer-public-rest-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<jaxrs.impl>jersey</jaxrs.impl>
|
||||
<spring.version>5.2.14.RELEASE</spring.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>commons-cli</groupId>
|
||||
<artifactId>commons-cli</artifactId>
|
||||
<version>1.5.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${project.parent.groupId}</groupId>
|
||||
<artifactId>buxfer-public-rest-client</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<classifier>jersey</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.inteligr8.polygon</groupId>
|
||||
<artifactId>polygon-public-rest-client</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<classifier>jersey</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.core</groupId>
|
||||
<artifactId>jersey-client</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.ext</groupId>
|
||||
<artifactId>jersey-proxy-client</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.inject</groupId>
|
||||
<artifactId>jersey-hk2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.media</groupId>
|
||||
<artifactId>jersey-media-json-jackson</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j-impl</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<version>${spring.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.9</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- This plugin causes the packaging of a JAR with embedded dependencies -->
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>main-jar</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.inteligr8.buxfer.CLI</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
16
buxfer-cli/src/assembly/resources/log4j2.xml
Normal file
16
buxfer-cli/src/assembly/resources/log4j2.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="com.inteligr8.buxfer" level="info" additivity="false">
|
||||
<AppenderRef ref="Console" />
|
||||
</Logger>
|
||||
<Root level="warn">
|
||||
<AppenderRef ref="Console" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
@ -0,0 +1,9 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
|
||||
public interface BuxferTransactionParser {
|
||||
|
||||
ParsedTransaction parse(Transaction buxferTx) throws InterruptedException;
|
||||
|
||||
}
|
64
buxfer-cli/src/main/java/com/inteligr8/buxfer/CLI.java
Normal file
64
buxfer-cli/src/main/java/com/inteligr8/buxfer/CLI.java
Normal file
@ -0,0 +1,64 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.cli.HelpFormatter;
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.apache.commons.cli.ParseException;
|
||||
|
||||
public class CLI {
|
||||
|
||||
public static void main(String[] args) throws ParseException, IOException, InterruptedException {
|
||||
Options options = new Options();
|
||||
if (args.length == 0) {
|
||||
help(options, null, null, "A function is required");
|
||||
return;
|
||||
} else if (args[0].equals("help")) {
|
||||
help(options, null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
String func = args[0];
|
||||
String[] subargs = Arrays.copyOfRange(args, 1, args.length);
|
||||
|
||||
switch (func.toLowerCase()) {
|
||||
case "investdetail":
|
||||
InvestNormalizeCLI.main(subargs);
|
||||
break;
|
||||
case "investgl":
|
||||
InvestGainsLossesCLI.main(subargs);
|
||||
break;
|
||||
case "writer":
|
||||
WriterCLI.main(subargs);
|
||||
break;
|
||||
default:
|
||||
help(options, null, null, "The function '" + func + "' is not valid");
|
||||
}
|
||||
}
|
||||
|
||||
static void help(Options options, String extraCommand, List<String> extraLines, String message) {
|
||||
System.err.println(message);
|
||||
help(options, extraCommand, extraLines);
|
||||
}
|
||||
|
||||
static void help(Options options, String extraCommand, List<String> extraLines) {
|
||||
System.out.println("A set of Buxfer tools");
|
||||
System.out.print("usage: java -jar buxfer-cli-*.jar");
|
||||
System.out.print(" ( InvestDetail | InvestGL | Writer )");
|
||||
if (extraCommand != null)
|
||||
System.out.print(" " + extraCommand);
|
||||
System.out.println();
|
||||
HelpFormatter help = new HelpFormatter();
|
||||
help.printOptions(new PrintWriter(System.out, true), 120, options, 3, 3);
|
||||
System.out.println("InvestDetail: Reformats descriptions and sets the appropriate transaction types for investments");
|
||||
System.out.println(" InvestGL: After 'InvestDetail', compute gains as transactions and reconcile");
|
||||
System.out.println(" Writer: Writes transactions to external file");
|
||||
if (extraLines != null)
|
||||
for (String extraLine : extraLines)
|
||||
System.out.println(extraLine);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
|
||||
public class CsvTransactionWriter implements Observer<Transaction> {
|
||||
|
||||
private final OutputStream ostream;
|
||||
|
||||
public CsvTransactionWriter(OutputStream ostream) {
|
||||
this.ostream = ostream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int observed(Transaction tx) throws IOException {
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,308 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.cli.CommandLine;
|
||||
import org.apache.commons.cli.DefaultParser;
|
||||
import org.apache.commons.cli.Option;
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.apache.commons.cli.ParseException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.inteligr8.buxfer.api.CommandApi;
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.Transaction.Status;
|
||||
import com.inteligr8.buxfer.model.Transaction.Type;
|
||||
import com.inteligr8.buxfer.model.TransactionsResponse;
|
||||
|
||||
public class InvestGainsLossesCLI {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(InvestGainsLossesCLI.class);
|
||||
|
||||
private static final Pattern splitFormat =
|
||||
Pattern.compile("Split ([A-Z]+) ([0-9]+) to ([0-9]+)");
|
||||
|
||||
private static final Map<String, Double> unsoldShares = new HashMap<>();
|
||||
private static final Map<String, List<TransactionWrapper>> unsoldBuys = new HashMap<>();
|
||||
|
||||
public static void main(String[] args) throws ParseException, IOException, InterruptedException {
|
||||
Options options = buildOptions();
|
||||
String extraCommand = "[options] \"accountName\"";
|
||||
List<String> extraLines = Arrays.asList("accountName: A Buxfer account name");
|
||||
|
||||
CommandLine cli = new DefaultParser().parse(options, args);
|
||||
if (cli.getArgList().isEmpty()) {
|
||||
CLI.help(options, extraCommand, extraLines, "An account name is required");
|
||||
return;
|
||||
} else if (cli.hasOption("help")) {
|
||||
CLI.help(options, extraCommand, extraLines);
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean taxExempt = cli.hasOption("tax-exempt");
|
||||
final boolean dryRun = cli.hasOption("dry-run");
|
||||
final int searchLimit = Integer.valueOf(cli.getOptionValue("limit", "0"));
|
||||
|
||||
String buxferAccountName = cli.getArgList().iterator().next();
|
||||
final CommandApi bapi = findBuxferApi(cli);
|
||||
|
||||
Observer<Transaction> observer = new Observer<Transaction>() {
|
||||
@Override
|
||||
public int observed(Transaction tx) throws IOException, InterruptedException {
|
||||
logger.debug("observed tx: {}", tx.getId());
|
||||
|
||||
try {
|
||||
ParsedTransaction ptx = NormalizedParser.getInstance().parse(tx);
|
||||
if (!ptx.getType().equals(tx.getType()))
|
||||
throw new IllegalStateException("The tx " + tx.getId() + " description and its type conflict: one 'Buy' one 'Sell'");
|
||||
|
||||
TransactionWrapper txw = new TransactionWrapper(ptx);
|
||||
switch (tx.getType()) {
|
||||
case Buy:
|
||||
registerBuy(txw);
|
||||
break;
|
||||
case Sell:
|
||||
return processSell(txw, bapi, taxExempt, dryRun);
|
||||
default:
|
||||
logger.debug("ignoring: " + tx.getDescription());
|
||||
}
|
||||
} catch (IllegalArgumentException iae) {
|
||||
Matcher matcher = splitFormat.matcher(tx.getDescription());
|
||||
if (matcher.matches()) {
|
||||
String symbol = matcher.group(1);
|
||||
int fromSecurities = Integer.valueOf(matcher.group(2));
|
||||
int toSecurities = Integer.valueOf(matcher.group(3));
|
||||
float ratio = 1f * toSecurities / fromSecurities;
|
||||
|
||||
logger.debug("splitting {} with ratio {}", symbol, ratio);
|
||||
|
||||
Double outstandingSecurities = unsoldShares.get(symbol);
|
||||
if (outstandingSecurities != null)
|
||||
unsoldShares.put(symbol, outstandingSecurities * ratio);
|
||||
|
||||
if (unsoldBuys.get(symbol) != null)
|
||||
for (TransactionWrapper txw : unsoldBuys.get(symbol))
|
||||
txw.recordSplit(ratio);
|
||||
} else {
|
||||
logger.debug("ignoring: " + tx.getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
searchTransactions(bapi, buxferAccountName, searchLimit, observer);
|
||||
|
||||
for (List<TransactionWrapper> txws : unsoldBuys.values()) {
|
||||
for (TransactionWrapper txw : txws) {
|
||||
if (txw.getParsedTransaction().getExpirationDate() != null) {
|
||||
if (LocalDate.now().isAfter(txw.getParsedTransaction().getExpirationDate())) {
|
||||
recordCapitalGain(txw.getOutstandingSecurities(), 0.0, txw.getParsedTransaction().getExpirationDate(), txw, bapi, taxExempt, dryRun);
|
||||
|
||||
if (!dryRun)
|
||||
bapi.editTransaction(txw.getTransaction().getId(), txw.getParsedTransaction().getNormalizedDescription(), null, Status.Reconciled);
|
||||
}
|
||||
} else {
|
||||
long days = LocalDate.now().toEpochDay() - txw.getTransaction().getDate().toEpochDay();
|
||||
logger.debug("unreconciled tx {}: {} ({} days)", txw.getTransaction().getId(), txw.getTransaction().getDescription(), days);
|
||||
|
||||
String description = txw.getDescription();
|
||||
if (!description.equals(txw.getTransaction().getDescription())) {
|
||||
logger.debug("updating partially reconciled tx {}: {}", txw.getTransaction().getId(), description);
|
||||
if (!dryRun)
|
||||
bapi.editTransaction(txw.getTransaction().getId(), description, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerBuy(TransactionWrapper txw) {
|
||||
Double unsoldSecurities = unsoldShares.get(txw.getParsedTransaction().getKey());
|
||||
unsoldSecurities = unsoldSecurities == null ? txw.getSplitAdjustedSecurities() : (unsoldSecurities + txw.getSplitAdjustedSecurities());
|
||||
unsoldShares.put(txw.getParsedTransaction().getKey(), unsoldSecurities);
|
||||
if (logger.isDebugEnabled())
|
||||
logger.debug("{} shares of: {}", unsoldShares.get(txw.getParsedTransaction().getKey()), txw.getParsedTransaction().getKey());
|
||||
|
||||
List<TransactionWrapper> txws = unsoldBuys.get(txw.getParsedTransaction().getKey());
|
||||
if (txws == null)
|
||||
unsoldBuys.put(txw.getParsedTransaction().getKey(), txws = new LinkedList<TransactionWrapper>());
|
||||
logger.debug("{} outstanding buys of: {}", txws.size()+1, txw.getParsedTransaction().getKey());
|
||||
txws.add(txw);
|
||||
}
|
||||
|
||||
private static int processSell(TransactionWrapper txw, CommandApi api, boolean taxExempt, boolean dryRun) {
|
||||
Double outstandingSecurities = unsoldShares.get(txw.getParsedTransaction().getKey());
|
||||
if (outstandingSecurities != null && outstandingSecurities < 0) {
|
||||
logger.debug("ignoring: {}", txw.getParsedTransaction().getKey());
|
||||
// security processing ignored
|
||||
return 0;
|
||||
} else if (outstandingSecurities == null || outstandingSecurities < txw.getSplitAdjustedSecurities()) {
|
||||
logger.warn("'" + txw.getParsedTransaction().getKey() + "' is out of shares; data integrity issue; not processing anymore sells on that security");
|
||||
unsoldShares.put(txw.getParsedTransaction().getKey(), Double.MIN_VALUE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
unsoldShares.put(txw.getParsedTransaction().getKey(), outstandingSecurities.doubleValue() - txw.getSplitAdjustedSecurities());
|
||||
if (logger.isDebugEnabled())
|
||||
logger.debug("{} shares of: {}", unsoldShares.get(txw.getParsedTransaction().getKey()), txw.getParsedTransaction().getKey());
|
||||
|
||||
int reconciled = 0;
|
||||
String symbolTag = "Investment / Security / " + txw.getParsedTransaction().getSecuritySymbol();
|
||||
|
||||
List<TransactionWrapper> buyTxws = unsoldBuys.get(txw.getParsedTransaction().getKey());
|
||||
ListIterator<TransactionWrapper> i = buyTxws.listIterator();
|
||||
//ListIterator<TransactionWrapper> i = buyTxws.listIterator(buyTxws.size());
|
||||
while (i.hasNext() && txw.getOutstandingSecurities() > 0) {
|
||||
TransactionWrapper buyTxw = i.next();
|
||||
logger.debug("reconciling with buy txId: {}", buyTxw.getTransaction().getId());
|
||||
|
||||
double sharesToDrain = txw.getOutstandingSecurities();
|
||||
if (buyTxw.getOutstandingSecurities() <= sharesToDrain) {
|
||||
sharesToDrain = buyTxw.getOutstandingSecurities();
|
||||
i.remove();
|
||||
}
|
||||
|
||||
double sellValueDrained = txw.decrementOutstandingSecurities(sharesToDrain);
|
||||
|
||||
recordCapitalGain(sharesToDrain, sellValueDrained, txw.getTransaction().getDate(), buyTxw, api, taxExempt, dryRun);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
logger.debug("reconciled sell txId: {}", txw.getTransaction().getId());
|
||||
api.editTransaction(txw.getTransaction().getId(), null, symbolTag, Status.Reconciled);
|
||||
reconciled++;
|
||||
}
|
||||
|
||||
return reconciled;
|
||||
}
|
||||
|
||||
private static boolean recordCapitalGain(double sharesToDrain, double sellValueDrained, LocalDate sellDate, TransactionWrapper txw, CommandApi api, boolean taxExempt, boolean dryRun) {
|
||||
logger.debug("reconciling {} securities", sharesToDrain);
|
||||
|
||||
double buyValueDrained = txw.decrementOutstandingSecurities(sharesToDrain);
|
||||
double gain = Math.round((sellValueDrained - buyValueDrained) * 100) / 100.0;
|
||||
|
||||
long days = sellDate.toEpochDay() - txw.getTransaction().getDate().toEpochDay();
|
||||
boolean longTerm = sellDate.minusYears(1L).plusDays(1).isAfter(txw.getTransaction().getDate());
|
||||
|
||||
String description = days + " Day " + (gain > 0.0 ? "Gain" : "Loss") + " " + txw.getParsedTransaction().getKey();
|
||||
|
||||
String capitalGainTag = "Investment / Capital Gain / ";
|
||||
if (taxExempt) capitalGainTag += "Exempt";
|
||||
else if (longTerm) capitalGainTag += "Long Term";
|
||||
else capitalGainTag += "Short Term";
|
||||
String symbolTag = "Investment / Security / " + txw.getParsedTransaction().getSecuritySymbol();
|
||||
|
||||
logger.debug("adding capital gain {}: {}", gain, description);
|
||||
|
||||
if (!dryRun) {
|
||||
Long fromAccountId = (gain < 0.0) ? txw.getTransaction().getAccountId() : null;
|
||||
Long toAccountId = (gain < 0.0) ? null : txw.getTransaction().getAccountId();
|
||||
|
||||
api.addTransaction(
|
||||
Type.Transfer.getOutgoingValue(),
|
||||
fromAccountId,
|
||||
toAccountId,
|
||||
sellDate,
|
||||
description,
|
||||
gain,
|
||||
symbolTag + "," + capitalGainTag,
|
||||
Status.Reconciled);
|
||||
|
||||
if (txw.getOutstandingSecurities() == 0) {
|
||||
logger.debug("reconciled buy txId: {}", txw.getTransaction().getId());
|
||||
api.editTransaction(txw.getTransaction().getId(), txw.getDescription(), null, Status.Reconciled);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void searchTransactions(CommandApi api, String buxferAccountName, int limit, Observer<Transaction> observer) throws IOException, InterruptedException {
|
||||
TransactionsResponse response = api.getTransactionsInAccount(buxferAccountName, null, null, Status.Cleared, 1).getResponse();
|
||||
|
||||
long total = response.getTotalItems();
|
||||
long maxToProcess = limit == 0 ? Long.MAX_VALUE : (long)limit;
|
||||
int pages = (int)((total-1) / 100) + 1;
|
||||
long processed = 0L;
|
||||
long reconciled = 0L;
|
||||
|
||||
// must go in reverse
|
||||
for (int p = pages; p > 0 && processed < maxToProcess; p--) {
|
||||
logger.debug("searching '{}' page {}", buxferAccountName, p);
|
||||
response = api.getTransactionsInAccount(buxferAccountName, null, null, Status.Cleared, p).getResponse();
|
||||
if ((total-reconciled) != response.getTotalItems()) {
|
||||
logger.error("Transaction count changed unexpectedly while processing: {} => {}", total-reconciled, response.getTotalItems());
|
||||
return;
|
||||
}
|
||||
|
||||
Collections.sort(response.getItems(), new Comparator<Transaction>() {
|
||||
@Override
|
||||
public int compare(Transaction tx1, Transaction tx2) {
|
||||
int compare = tx1.getDate().compareTo(tx2.getDate());
|
||||
if (compare == 0)
|
||||
// we want buys before sells
|
||||
compare = tx1.getDescription().compareTo(tx2.getDescription());
|
||||
return compare;
|
||||
}
|
||||
});
|
||||
|
||||
for (Transaction tx : response.getItems()) {
|
||||
try {
|
||||
reconciled += observer.observed(tx);
|
||||
} catch (IllegalArgumentException | IllegalStateException ie) {
|
||||
logger.debug("transaction cannot be processed: " + ie.getMessage());
|
||||
}
|
||||
|
||||
processed++;
|
||||
if (processed >= maxToProcess)
|
||||
break;
|
||||
}
|
||||
|
||||
logger.info("Processed page {} ({} txs; {} existing tx reconciled)", p, processed, reconciled);
|
||||
|
||||
// let the Buxfer indexer catch up
|
||||
Thread.sleep(5000L);
|
||||
}
|
||||
|
||||
logger.info("Reconciled {} existing transactions ({} txs)", reconciled, processed);
|
||||
}
|
||||
|
||||
private static Options buildOptions() {
|
||||
return new Options()
|
||||
.addOption(new Option("bu", "buxfer-email", true, "A Buxfer email for authentication"))
|
||||
.addOption(new Option("bp", "buxfer-password", true, "A Buxfer password for authentication"))
|
||||
.addOption(new Option("t", "tax-exempt", false, "Tag capital gains/losses and dividends as tax exempt"))
|
||||
.addOption(new Option("dr", "dry-run", false, "Do not updated Buxfer (for testing)"))
|
||||
.addOption(new Option("l", "limit", true, "Only process this many transactions (for testing)"));
|
||||
}
|
||||
|
||||
private static CommandApi findBuxferApi(CommandLine cli) {
|
||||
BuxferClientConfiguration config = new BuxferClientConfiguration();
|
||||
config.setBaseUrl("https://www.buxfer.com");
|
||||
|
||||
if (cli.hasOption("buxfer-email"))
|
||||
config.setAuthEmail(cli.getOptionValue("buxfer-email"));
|
||||
if (cli.hasOption("buxfer-password"))
|
||||
config.setAuthPassword(cli.getOptionValue("buxfer-password"));
|
||||
|
||||
BuxferClientJerseyImpl client = new BuxferClientJerseyImpl(config);
|
||||
return client.getApi().getCommandApi();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.cli.CommandLine;
|
||||
import org.apache.commons.cli.DefaultParser;
|
||||
import org.apache.commons.cli.Option;
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.apache.commons.cli.ParseException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.Transaction.Status;
|
||||
import com.inteligr8.buxfer.model.TransactionsResponse;
|
||||
import com.inteligr8.polygon.PolygonClientConfiguration;
|
||||
import com.inteligr8.polygon.PolygonClientJerseyImpl;
|
||||
import com.inteligr8.polygon.PolygonPublicRestApi;
|
||||
|
||||
public class InvestNormalizeCLI {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(InvestNormalizeCLI.class);
|
||||
|
||||
public static void main(String[] args) throws ParseException, IOException, InterruptedException {
|
||||
Options options = buildOptions();
|
||||
String extraCommand = "[options] \"accountName\"";
|
||||
List<String> extraLines = Arrays.asList("accountName: A Buxfer account name");
|
||||
|
||||
CommandLine cli = new DefaultParser().parse(options, args);
|
||||
if (cli.getArgList().isEmpty()) {
|
||||
CLI.help(options, extraCommand, extraLines, "An account name is required");
|
||||
return;
|
||||
} else if (cli.hasOption("help")) {
|
||||
CLI.help(options, extraCommand, extraLines);
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean dryRun = cli.hasOption("dry-run");
|
||||
final int searchLimit = Integer.valueOf(cli.getOptionValue("limit", "0"));
|
||||
|
||||
String buxferAccountName = cli.getArgList().iterator().next();
|
||||
final BuxferPublicRestApi bapi = findBuxferApi(cli);
|
||||
final PolygonPublicRestApi papi = findPolygonApi(cli);
|
||||
|
||||
final List<BuxferTransactionParser> parsers = Arrays.asList(
|
||||
TdAmeritradeParser.getInstance(),
|
||||
SofiInvestParser.getInstance(papi));
|
||||
|
||||
Observer<Transaction> observer = new Observer<Transaction>() {
|
||||
@Override
|
||||
public int observed(Transaction tx) throws IOException, InterruptedException {
|
||||
logger.debug("observed tx: {}", tx.getId());
|
||||
|
||||
// ignore null/empty descriptions
|
||||
if (tx.getDescription() == null || tx.getDescription().length() == 0)
|
||||
return 0;
|
||||
|
||||
try {
|
||||
NormalizedParser.getInstance().parse(tx);
|
||||
logger.trace("tx {} already formatted/processed: {}", tx.getId(), tx.getDescription());
|
||||
return 0;
|
||||
} catch (IllegalArgumentException iae) {
|
||||
// continue
|
||||
}
|
||||
|
||||
for (BuxferTransactionParser parser : parsers) {
|
||||
try {
|
||||
ParsedTransaction ptx = parser.parse(tx);
|
||||
logger.debug("tx {} formatted: {} => {}", tx.getId(), tx.getDescription(), ptx.getNormalizedDescription());
|
||||
|
||||
if (!tx.getType().equals(ptx.getType()))
|
||||
logger.debug("tx {} re-typed: {} => {}", tx.getId(), tx.getType(), ptx.getType());
|
||||
|
||||
if (!dryRun)
|
||||
bapi.getCommandApi().editTransaction(tx.getId(), ptx.getType().getOutgoingValue(), ptx.getNormalizedDescription(),
|
||||
"Investment / Security / " + ptx.getSecuritySymbol(), null);
|
||||
return 1;
|
||||
} catch (IllegalArgumentException iae) {
|
||||
// try another
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("tx {} cannot be formatted: {}", tx.getId(), tx.getDescription());
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
searchTransactions(bapi, buxferAccountName, searchLimit, observer);
|
||||
}
|
||||
|
||||
private static void searchTransactions(BuxferPublicRestApi api, String buxferAccountName, int limit, Observer<Transaction> observer) throws IOException, InterruptedException {
|
||||
long maxToProcess = limit == 0 ? Long.MAX_VALUE : (long)limit;
|
||||
long total = -1L;
|
||||
long processed = 0L;
|
||||
long updated = 0L;
|
||||
int pages = 1;
|
||||
for (int p = 1; p <= pages && processed < maxToProcess; p++) {
|
||||
logger.debug("searching '{}' page {}", buxferAccountName, p);
|
||||
TransactionsResponse response = api.getCommandApi().getTransactionsInAccount(buxferAccountName, null, null, Status.Cleared, p).getResponse();
|
||||
if (total < 0L) {
|
||||
total = response.getTotalItems();
|
||||
pages = (int)((response.getTotalItems()-1) / 100) + 1;
|
||||
logger.debug("found {} total transactions covering {} pages", total, pages);
|
||||
} else if (total != response.getTotalItems()) {
|
||||
logger.error("Transaction count changed while processing");
|
||||
return;
|
||||
}
|
||||
|
||||
for (Transaction tx : response.getItems()) {
|
||||
updated += observer.observed(tx);
|
||||
processed++;
|
||||
if (processed >= maxToProcess)
|
||||
break;
|
||||
}
|
||||
|
||||
logger.info("Processed page {} ({} txs; {} updated)", p, processed, updated);
|
||||
}
|
||||
|
||||
logger.info("Updated {} transactions ({} txs)", updated, processed);
|
||||
}
|
||||
|
||||
private static Options buildOptions() {
|
||||
return new Options()
|
||||
.addOption(new Option("bu", "buxfer-email", true, "A Buxfer email for authentication"))
|
||||
.addOption(new Option("bp", "buxfer-password", true, "A Buxfer password for authentication"))
|
||||
.addOption(new Option("pak", "polygon-apiKey", true, "A Polygon.IO API key for authentication"))
|
||||
.addOption(new Option("dr", "dry-run", false, "Do not updated Buxfer (for testing)"))
|
||||
.addOption(new Option("l", "limit", true, "Only process this many transactions (for testing)"));
|
||||
}
|
||||
|
||||
private static BuxferPublicRestApi findBuxferApi(CommandLine cli) {
|
||||
BuxferClientConfiguration config = new BuxferClientConfiguration();
|
||||
config.setBaseUrl("https://www.buxfer.com");
|
||||
|
||||
if (cli.hasOption("buxfer-email"))
|
||||
config.setAuthEmail(cli.getOptionValue("buxfer-email"));
|
||||
if (cli.hasOption("buxfer-password"))
|
||||
config.setAuthPassword(cli.getOptionValue("buxfer-password"));
|
||||
|
||||
BuxferClientJerseyImpl client = new BuxferClientJerseyImpl(config);
|
||||
return client.getApi();
|
||||
}
|
||||
|
||||
private static PolygonPublicRestApi findPolygonApi(CommandLine cli) {
|
||||
PolygonClientConfiguration config = new PolygonClientConfiguration();
|
||||
config.setBaseUrl("https://api.polygon.io");
|
||||
|
||||
if (cli.hasOption("polygon-apiKey"))
|
||||
config.setApiKey(cli.getOptionValue("polygon-apiKey"));
|
||||
|
||||
PolygonClientJerseyImpl client = new PolygonClientJerseyImpl(config);
|
||||
return client.getApi();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
|
||||
public class JsonTransactionWriter implements Observer<Transaction> {
|
||||
|
||||
private final ObjectMapper om = new ObjectMapper();
|
||||
private final OutputStream ostream;
|
||||
|
||||
public JsonTransactionWriter(OutputStream ostream) {
|
||||
this.ostream = ostream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int observed(Transaction tx) throws IOException {
|
||||
this.om.writeValue(this.ostream, tx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class MyStockPortfolioCsvTransactionWriter extends CsvTransactionWriter {
|
||||
|
||||
public MyStockPortfolioCsvTransactionWriter(OutputStream ostream) {
|
||||
super(ostream);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.Transaction.Type;
|
||||
|
||||
public class NormalizedDividendTransaction implements ParsedTransaction {
|
||||
|
||||
private final Pattern descriptionFormat =
|
||||
Pattern.compile("((Qualified|Ordinary) )?Dividend ([A-Z]+)");
|
||||
|
||||
private final Transaction tx;
|
||||
private final String symbol;
|
||||
|
||||
public NormalizedDividendTransaction(Transaction tx) {
|
||||
this.tx = tx;
|
||||
|
||||
Matcher matcher = this.descriptionFormat.matcher(tx.getDescription());
|
||||
if (!matcher.find())
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
this.symbol = matcher.group(3).toUpperCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transaction getTransaction() {
|
||||
return this.tx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNormalizedDescription() {
|
||||
return this.tx.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.Dividend;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getSecurities() {
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSecuritySymbol() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getPricePerSecurity() {
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.Transaction.Type;
|
||||
|
||||
public class NormalizedOptionTransaction implements ParsedTransaction, PartiallyReconciledTransaction {
|
||||
|
||||
private final Pattern descriptionFormat =
|
||||
Pattern.compile("(Bought|Sold) ([0-9\\.]+) ([A-Z]+) (2[0-9]{3}-[0-9]{2}-[0-9]{2}) ([0-9\\.]+) (PUT|CALL) @ ([0-9\\.]+)(; ([0-9\\.]+) unsold)?");
|
||||
|
||||
private final Transaction tx;
|
||||
private final Type type;
|
||||
private final double securities;
|
||||
private final Double unsoldSecurities;
|
||||
private final String symbol;
|
||||
private final LocalDate expirationDate;
|
||||
private final double strikePrice;
|
||||
private final ContractType contractType;
|
||||
private final double perSecurityPrice;
|
||||
private final String description;
|
||||
|
||||
public NormalizedOptionTransaction(Transaction tx) {
|
||||
this.tx = tx;
|
||||
|
||||
Matcher matcher = this.descriptionFormat.matcher(tx.getDescription());
|
||||
if (!matcher.find())
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
String buysell = matcher.group(1);
|
||||
this.securities = Double.valueOf(matcher.group(2));
|
||||
this.symbol = matcher.group(3).toUpperCase();
|
||||
this.expirationDate = LocalDate.parse(matcher.group(4));
|
||||
this.strikePrice = Double.valueOf(matcher.group(5));
|
||||
this.contractType = ContractType.valueOf(matcher.group(6));
|
||||
this.perSecurityPrice = Double.valueOf(matcher.group(7));
|
||||
String unsoldSecurities = matcher.group(9);
|
||||
|
||||
this.type = this.determineTransactionType(buysell);
|
||||
this.unsoldSecurities = unsoldSecurities == null ? null : Double.valueOf(unsoldSecurities);
|
||||
|
||||
this.description = new StringBuilder()
|
||||
.append(buysell).append(' ')
|
||||
.append(NumberFormatFactory.getSecuritiesFormatter().format(this.securities)).append(' ')
|
||||
.append(this.symbol).append(' ')
|
||||
.append(this.expirationDate.toString()).append(' ')
|
||||
.append(NumberFormatFactory.getStrikePriceFormatter().format(this.strikePrice)).append(' ')
|
||||
.append(this.contractType.toString()).append(" @ ")
|
||||
.append(NumberFormatFactory.getPriceFormatter().format(this.perSecurityPrice)).toString();
|
||||
}
|
||||
|
||||
private Type determineTransactionType(String buysell) {
|
||||
buysell = buysell.toLowerCase();
|
||||
if (buysell.charAt(0) == 'b') {
|
||||
return Type.Buy;
|
||||
} else if (buysell.charAt(0) == 's') {
|
||||
return Type.Sell;
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transaction getTransaction() {
|
||||
return this.tx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNormalizedDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return this.symbol + ":" + this.expirationDate + ":" + this.strikePrice + ":" + this.contractType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getSecurities() {
|
||||
return this.securities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getUnsoldSecurities() {
|
||||
return this.unsoldSecurities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSecuritySymbol() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getPricePerSecurity() {
|
||||
return this.perSecurityPrice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContractType getContractType() {
|
||||
return this.contractType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate getExpirationDate() {
|
||||
return this.expirationDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getStrikePrice() {
|
||||
return this.strikePrice;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
|
||||
public class NormalizedParser implements BuxferTransactionParser {
|
||||
|
||||
private static final NormalizedParser INSTANCE = new NormalizedParser();
|
||||
|
||||
public static NormalizedParser getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private NormalizedParser() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParsedTransaction parse(Transaction buxferTx) {
|
||||
try {
|
||||
return new NormalizedOptionTransaction(buxferTx);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
try {
|
||||
return new NormalizedStockTransaction(buxferTx);
|
||||
} catch (IllegalArgumentException iae2) {
|
||||
return new NormalizedDividendTransaction(buxferTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.Transaction.Type;
|
||||
|
||||
public class NormalizedStockTransaction implements ParsedTransaction, PartiallyReconciledTransaction {
|
||||
|
||||
private final Pattern descriptionFormat =
|
||||
Pattern.compile("(Bought|Sold) ([0-9\\.]+) ([A-Z]+) @ ([0-9\\.]+)(; ([0-9\\.]+) unsold)?");
|
||||
|
||||
private final Transaction tx;
|
||||
private final Type type;
|
||||
private final double securities;
|
||||
private final Double unsoldSecurities;
|
||||
private final String symbol;
|
||||
private final String description;
|
||||
private final double perSecurityPrice;
|
||||
|
||||
public NormalizedStockTransaction(Transaction tx) {
|
||||
this.tx = tx;
|
||||
|
||||
Matcher matcher = this.descriptionFormat.matcher(tx.getDescription());
|
||||
if (!matcher.find())
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
String buysell = matcher.group(1);
|
||||
this.securities = Double.valueOf(matcher.group(2));
|
||||
this.symbol = matcher.group(3).toUpperCase();
|
||||
this.perSecurityPrice = Double.valueOf(matcher.group(4));
|
||||
String unsoldSecurities = matcher.group(6);
|
||||
|
||||
this.type = this.determineTransactionType(buysell);
|
||||
this.unsoldSecurities = unsoldSecurities == null ? null : Double.valueOf(unsoldSecurities);
|
||||
|
||||
this.description = new StringBuilder()
|
||||
.append(buysell).append(' ')
|
||||
.append(NumberFormatFactory.getSecuritiesFormatter().format(this.securities)).append(' ')
|
||||
.append(this.symbol).append(" @ ")
|
||||
.append(NumberFormatFactory.getPriceFormatter().format(this.perSecurityPrice)).toString();
|
||||
}
|
||||
|
||||
private Type determineTransactionType(String buysell) {
|
||||
buysell = buysell.toLowerCase();
|
||||
if (buysell.charAt(0) == 'b') {
|
||||
return Type.Buy;
|
||||
} else if (buysell.charAt(0) == 's') {
|
||||
return Type.Sell;
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transaction getTransaction() {
|
||||
return this.tx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNormalizedDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getSecurities() {
|
||||
return this.securities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getUnsoldSecurities() {
|
||||
return this.unsoldSecurities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSecuritySymbol() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getPricePerSecurity() {
|
||||
return this.perSecurityPrice;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.math.RoundingMode;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
public class NumberFormatFactory {
|
||||
|
||||
private static final NumberFormat strikePriceFormatter = NumberFormat.getNumberInstance();
|
||||
static {
|
||||
strikePriceFormatter.setMaximumFractionDigits(4);
|
||||
strikePriceFormatter.setMinimumFractionDigits(1);
|
||||
strikePriceFormatter.setRoundingMode(RoundingMode.HALF_UP);
|
||||
strikePriceFormatter.setGroupingUsed(false);
|
||||
}
|
||||
|
||||
private static final NumberFormat priceFormatter = NumberFormat.getNumberInstance();
|
||||
static {
|
||||
priceFormatter.setMaximumFractionDigits(4);
|
||||
priceFormatter.setMinimumFractionDigits(0);
|
||||
priceFormatter.setRoundingMode(RoundingMode.HALF_UP);
|
||||
priceFormatter.setGroupingUsed(false);
|
||||
}
|
||||
|
||||
private static final NumberFormat securityFormatter = NumberFormat.getNumberInstance();
|
||||
static {
|
||||
securityFormatter.setMaximumFractionDigits(12);
|
||||
securityFormatter.setMinimumFractionDigits(0);
|
||||
securityFormatter.setRoundingMode(RoundingMode.HALF_UP);
|
||||
securityFormatter.setGroupingUsed(false);
|
||||
}
|
||||
|
||||
public static NumberFormat getStrikePriceFormatter() {
|
||||
return strikePriceFormatter;
|
||||
}
|
||||
|
||||
public static NumberFormat getPriceFormatter() {
|
||||
return priceFormatter;
|
||||
}
|
||||
|
||||
public static NumberFormat getSecuritiesFormatter() {
|
||||
return securityFormatter;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
public class NumericRange<T extends Number> {
|
||||
|
||||
private final T from;
|
||||
private final T to;
|
||||
|
||||
public NumericRange(T fromAndTo) {
|
||||
this.from = fromAndTo;
|
||||
this.to = fromAndTo;
|
||||
}
|
||||
|
||||
public NumericRange(T from, T to) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
public T getFrom() {
|
||||
return this.from;
|
||||
}
|
||||
|
||||
public T getTo() {
|
||||
return this.to;
|
||||
}
|
||||
|
||||
public boolean isPoint() {
|
||||
return this.from.equals(this.to);
|
||||
}
|
||||
|
||||
public double getMidpoint() {
|
||||
return (this.from.doubleValue() + this.to.doubleValue()) / 2.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.from + "-" + this.to;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface Observer<T> {
|
||||
|
||||
int observed(T t) throws IOException, InterruptedException;
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.Transaction.Type;
|
||||
|
||||
public interface ParsedTransaction {
|
||||
|
||||
public enum ContractType {
|
||||
PUT,
|
||||
CALL
|
||||
}
|
||||
|
||||
/**
|
||||
* This provides the Buxfer transaction object and all of its raw meta-data.
|
||||
*/
|
||||
Transaction getTransaction();
|
||||
|
||||
String getNormalizedDescription();
|
||||
|
||||
/**
|
||||
* This provides a hash key for matching buys with sells. It should uniquely identify the security.
|
||||
*/
|
||||
String getKey();
|
||||
|
||||
/**
|
||||
* This provides the type of transaction as described in the transaction's description.
|
||||
*/
|
||||
Type getType();
|
||||
|
||||
/**
|
||||
* This provides the number of shares, option contracts, or even fractional shares (for crypto) in the transaction.
|
||||
*/
|
||||
double getSecurities();
|
||||
|
||||
/**
|
||||
* This provides only the security symbol of the transaction; not the expiration date or strike price included (for options).
|
||||
*/
|
||||
String getSecuritySymbol();
|
||||
|
||||
double getPricePerSecurity();
|
||||
|
||||
/**
|
||||
* This provides the option contract type: Put or Call
|
||||
*
|
||||
* @return The option contract type; null if not an option
|
||||
*/
|
||||
default ContractType getContractType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
default LocalDate getExpirationDate() {
|
||||
return null;
|
||||
}
|
||||
|
||||
default Double getStrikePrice() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
public interface PartiallyReconciledTransaction {
|
||||
|
||||
Double getUnsoldSecurities();
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.polygon.PolygonPublicRestApi;
|
||||
|
||||
public class SofiInvestParser implements BuxferTransactionParser {
|
||||
|
||||
private static SofiInvestParser INSTANCE;
|
||||
|
||||
public synchronized static SofiInvestParser getInstance(PolygonPublicRestApi papi) {
|
||||
if (INSTANCE == null)
|
||||
INSTANCE = new SofiInvestParser(papi);
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private final PolygonPublicRestApi papi;
|
||||
|
||||
private SofiInvestParser(PolygonPublicRestApi papi) {
|
||||
this.papi = papi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParsedTransaction parse(Transaction buxferTx) throws InterruptedException {
|
||||
return new SofiInvestTransaction(buxferTx, this.papi);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.Transaction.Type;
|
||||
import com.inteligr8.polygon.PolygonPublicRestApi;
|
||||
import com.inteligr8.polygon.model.StockDateSummary;
|
||||
|
||||
public class SofiInvestTransaction implements ParsedTransaction {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
private final Pattern descriptionFormat =
|
||||
Pattern.compile("(Buy|Sell|Dividend) ([A-Za-z]+)");
|
||||
|
||||
private static long nextExecutionTime = 0L;
|
||||
private final long polygonServiceThrottleTime = 20L * 1000L;
|
||||
|
||||
private final PolygonPublicRestApi papi;
|
||||
private final Transaction tx;
|
||||
private final Type type;
|
||||
private final NumericRange<? extends Number> securities;
|
||||
private final String symbol;
|
||||
private final String description;
|
||||
private final NumericRange<Double> perSecurityPrice;
|
||||
|
||||
public SofiInvestTransaction(Transaction tx, PolygonPublicRestApi papi) throws InterruptedException {
|
||||
this.papi = papi;
|
||||
this.tx = tx;
|
||||
|
||||
Matcher matcher = this.descriptionFormat.matcher(tx.getDescription());
|
||||
if (!matcher.find())
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
String buysell = matcher.group(1);
|
||||
this.symbol = matcher.group(2).toUpperCase();
|
||||
|
||||
this.type = this.determineTransactionType(buysell);
|
||||
|
||||
if (Type.Dividend.equals(this.type)) {
|
||||
this.description = "Dividend " + this.symbol;
|
||||
this.securities = null;
|
||||
this.perSecurityPrice = null;
|
||||
} else {
|
||||
this.throttle();
|
||||
|
||||
this.securities = this.estimateSecurites();
|
||||
this.perSecurityPrice = this.estimatePrice(securities);
|
||||
|
||||
String action = Type.Buy.equals(type) ? "Bought" : "Sold";
|
||||
this.description = new StringBuilder()
|
||||
.append(action).append(' ')
|
||||
.append(this.securities == null ? "NaN" : NumberFormatFactory.getSecuritiesFormatter().format(this.securities)).append(' ')
|
||||
.append(this.symbol).append(" @ ")
|
||||
.append(this.perSecurityPrice == null ? "NaN" : NumberFormatFactory.getPriceFormatter().format(this.perSecurityPrice)).toString();
|
||||
}
|
||||
}
|
||||
|
||||
private Type determineTransactionType(String buysell) {
|
||||
buysell = buysell.toLowerCase();
|
||||
if (buysell.charAt(0) == 'b') {
|
||||
return Type.Buy;
|
||||
} else if (buysell.charAt(0) == 's') {
|
||||
return Type.Sell;
|
||||
} else if (buysell.charAt(0) == 'd') {
|
||||
return Type.Dividend;
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void throttle() throws InterruptedException {
|
||||
// Polygon.IO throttling
|
||||
if (SofiInvestTransaction.nextExecutionTime > System.currentTimeMillis()) {
|
||||
long waitTime = SofiInvestTransaction.nextExecutionTime - System.currentTimeMillis();
|
||||
logger.debug("Throttled for Polygon.IO service for {} ms", waitTime);
|
||||
Thread.sleep(waitTime);
|
||||
}
|
||||
SofiInvestTransaction.nextExecutionTime = System.currentTimeMillis() + this.polygonServiceThrottleTime;
|
||||
}
|
||||
|
||||
protected NumericRange<? extends Number> estimateSecurites() {
|
||||
this.logger.debug("Searching for price activity for {} on {}", symbol, this.tx.getDate());
|
||||
StockDateSummary summary = this.papi.getStocksApi().getStockSummaryOnDate(this.symbol, this.tx.getDate(), false);
|
||||
if (summary.getLow() == null || summary.getHigh() == null)
|
||||
return null;
|
||||
|
||||
double maxFracShares = this.tx.getAmount() / summary.getLow();
|
||||
double minFracShares = this.tx.getAmount() / summary.getHigh();
|
||||
int maxShares = (int)Math.floor(maxFracShares);
|
||||
int minShares = (int)Math.ceil(minFracShares);
|
||||
|
||||
if (minShares == maxShares) {
|
||||
logger.debug("Found the highly likely shares: {}", minShares);
|
||||
return new NumericRange<Integer>(minShares);
|
||||
} else if (minShares < maxShares) {
|
||||
logger.info("Found a range of possible shares; needs manual update: {} => {}", this.tx.getDate(), this.tx.getDescription());
|
||||
return new NumericRange<Integer>(minShares, maxShares);
|
||||
} else if (maxFracShares - maxShares < 0.05) {
|
||||
logger.debug("Found the highly likely shares (w/ commission): {}", maxShares);
|
||||
return new NumericRange<Integer>(maxShares);
|
||||
} else {
|
||||
logger.info("Found a fractional range of possible shares; needs manual update: {} => {}", this.tx.getDate(), this.tx.getDescription());
|
||||
return new NumericRange<Double>(minFracShares, maxFracShares);
|
||||
}
|
||||
}
|
||||
|
||||
protected NumericRange<Double> estimatePrice(NumericRange<? extends Number> securities) {
|
||||
if (securities == null)
|
||||
return null;
|
||||
if (securities.isPoint())
|
||||
return new NumericRange<Double>(Math.round(this.tx.getAmount().doubleValue() * 100 / securities.getFrom().doubleValue()) / 100.0);
|
||||
|
||||
return new NumericRange<Double>(
|
||||
Math.round(this.tx.getAmount().doubleValue() * 100 / securities.getTo().doubleValue()) / 100.0,
|
||||
Math.round(this.tx.getAmount().doubleValue() * 100 / securities.getFrom().doubleValue()) / 100.0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transaction getTransaction() {
|
||||
return this.tx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNormalizedDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getSecurities() {
|
||||
if (this.securities == null) return Double.NaN;
|
||||
else if (this.securities.isPoint()) return this.securities.getFrom().doubleValue();
|
||||
else return this.securities.getMidpoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSecuritySymbol() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getPricePerSecurity() {
|
||||
if (this.perSecurityPrice == null) return Double.NaN;
|
||||
else if (this.perSecurityPrice.isPoint()) return this.perSecurityPrice.getFrom().doubleValue();
|
||||
else return this.perSecurityPrice.getMidpoint();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.Transaction.Type;
|
||||
|
||||
public class TdAmeritradeDividendTransaction implements ParsedTransaction {
|
||||
|
||||
private final Pattern descriptionFormat =
|
||||
Pattern.compile("(Qualified|Ordinary) Dividend \\(([A-Za-z]+)\\)");
|
||||
|
||||
private final Transaction tx;
|
||||
private final String symbol;
|
||||
private final String description;
|
||||
|
||||
public TdAmeritradeDividendTransaction(Transaction tx) {
|
||||
this.tx = tx;
|
||||
|
||||
Matcher matcher = this.descriptionFormat.matcher(tx.getDescription());
|
||||
if (!matcher.find())
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
String dividendType = matcher.group(1);
|
||||
this.symbol = matcher.group(2).toUpperCase();
|
||||
|
||||
this.description = new StringBuilder()
|
||||
.append(dividendType).append(" Dividend ")
|
||||
.append(this.symbol).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transaction getTransaction() {
|
||||
return this.tx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNormalizedDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.Dividend;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getSecurities() {
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSecuritySymbol() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getPricePerSecurity() {
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.Transaction.Type;
|
||||
|
||||
public class TdAmeritradeOptionTransaction implements ParsedTransaction {
|
||||
|
||||
private final Pattern descriptionFormat =
|
||||
Pattern.compile(".*(Bought|Sold)( To (Open|Close))? ([0-9\\.]+) ([A-Za-z]+) ([A-Za-z]+) ([0-9Xx]+) (2[0-9]{3}) ([0-9\\.]+) (Put|Call) @ ([0-9\\.]+)");
|
||||
|
||||
private final Transaction tx;
|
||||
private final Type type;
|
||||
private final int securities;
|
||||
private final String symbol;
|
||||
private final LocalDate expirationDate;
|
||||
private final double strikePrice;
|
||||
private final ContractType contractType;
|
||||
private final double perSecurityPrice;
|
||||
private final String description;
|
||||
|
||||
public TdAmeritradeOptionTransaction(Transaction tx) {
|
||||
this.tx = tx;
|
||||
|
||||
Matcher matcher = this.descriptionFormat.matcher(tx.getDescription());
|
||||
if (!matcher.find())
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
String buysell = matcher.group(1);
|
||||
//String openclose = matcher.group(3).toLowerCase();
|
||||
this.securities = Integer.valueOf(matcher.group(4));
|
||||
this.symbol = matcher.group(5).toUpperCase();
|
||||
String strikeMonth = matcher.group(6);
|
||||
String strikeDay = matcher.group(7);
|
||||
String strikeYear = matcher.group(8);
|
||||
this.strikePrice = Double.valueOf(matcher.group(9));
|
||||
this.contractType = ContractType.valueOf(matcher.group(10).toUpperCase());
|
||||
this.perSecurityPrice = Double.valueOf(matcher.group(11));
|
||||
|
||||
this.type = this.determineTransactionType(buysell);
|
||||
this.expirationDate = this.determineExpirationDate(strikeYear, strikeMonth, strikeDay);
|
||||
|
||||
String action = Type.Buy.equals(type) ? "Bought" : "Sold";
|
||||
this.description = new StringBuilder()
|
||||
.append(action).append(' ')
|
||||
.append(NumberFormatFactory.getSecuritiesFormatter().format(this.securities)).append(' ')
|
||||
.append(this.symbol).append(' ')
|
||||
.append(this.expirationDate.toString()).append(' ')
|
||||
.append(NumberFormatFactory.getStrikePriceFormatter().format(this.strikePrice)).append(' ')
|
||||
.append(this.contractType.toString()).append(" @ ")
|
||||
.append(NumberFormatFactory.getPriceFormatter().format(this.perSecurityPrice)).toString();
|
||||
}
|
||||
|
||||
private Type determineTransactionType(String buysell) {
|
||||
buysell = buysell.toLowerCase();
|
||||
if (buysell.charAt(0) == 'b') {
|
||||
return Type.Buy;
|
||||
} else if (buysell.charAt(0) == 's') {
|
||||
return Type.Sell;
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDate determineExpirationDate(String strikeYearStr, String strikeMonthStr, String strikeDayStr) {
|
||||
return this.determineExpirationDate(
|
||||
Integer.parseInt(strikeYearStr),
|
||||
this.parseStrikeMonth(strikeMonthStr),
|
||||
Integer.parseInt(strikeDayStr));
|
||||
}
|
||||
|
||||
private LocalDate determineExpirationDate(int strikeYear, Month strikeMonth, int strikeDay) {
|
||||
try {
|
||||
return LocalDate.of(Integer.valueOf(strikeYear), strikeMonth, Integer.valueOf(strikeDay));
|
||||
} catch (NumberFormatException nfe) {
|
||||
LocalDate expirationDate = LocalDate.of(Integer.valueOf(strikeYear), strikeMonth, 1);
|
||||
while (!DayOfWeek.FRIDAY.equals(expirationDate.getDayOfWeek()))
|
||||
expirationDate = expirationDate.plusDays(1L);
|
||||
return expirationDate.plusWeeks(2L); // 3rd friday of month
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transaction getTransaction() {
|
||||
return this.tx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNormalizedDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return this.symbol + ":" + this.expirationDate + ":" + this.strikePrice + ":" + this.contractType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getSecurities() {
|
||||
return (double)this.securities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSecuritySymbol() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getPricePerSecurity() {
|
||||
return this.perSecurityPrice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContractType getContractType() {
|
||||
return this.contractType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate getExpirationDate() {
|
||||
return this.expirationDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getStrikePrice() {
|
||||
return this.strikePrice;
|
||||
}
|
||||
|
||||
protected Month parseStrikeMonth(String month) {
|
||||
month = month.toLowerCase();
|
||||
switch (month.charAt(0)) {
|
||||
case 'f': return Month.FEBRUARY;
|
||||
case 's': return Month.SEPTEMBER;
|
||||
case 'o': return Month.OCTOBER;
|
||||
case 'n': return Month.NOVEMBER;
|
||||
case 'd': return Month.DECEMBER;
|
||||
case 'j':
|
||||
switch (month.substring(0, 3)) {
|
||||
case "jan": return Month.JANUARY;
|
||||
case "jun": return Month.JUNE;
|
||||
case "jul": return Month.JULY;
|
||||
default: return null;
|
||||
}
|
||||
case 'm':
|
||||
switch (month.charAt(2)) {
|
||||
case 'r': return Month.MARCH;
|
||||
case 'y': return Month.MAY;
|
||||
default: return null;
|
||||
}
|
||||
case 'a':
|
||||
switch (month.charAt(1)) {
|
||||
case 'p': return Month.APRIL;
|
||||
case 'u': return Month.AUGUST;
|
||||
default: return null;
|
||||
}
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
|
||||
public class TdAmeritradeParser implements BuxferTransactionParser {
|
||||
|
||||
private static final TdAmeritradeParser INSTANCE = new TdAmeritradeParser();
|
||||
|
||||
public static TdAmeritradeParser getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private TdAmeritradeParser() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParsedTransaction parse(Transaction buxferTx) {
|
||||
try {
|
||||
return new TdAmeritradeOptionTransaction(buxferTx);
|
||||
} catch (IllegalArgumentException iae) {
|
||||
try {
|
||||
return new TdAmeritradeStockTransaction(buxferTx);
|
||||
} catch (IllegalArgumentException iae2) {
|
||||
return new TdAmeritradeDividendTransaction(buxferTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.Transaction.Type;
|
||||
|
||||
public class TdAmeritradeStockTransaction implements ParsedTransaction {
|
||||
|
||||
private final Pattern descriptionFormat =
|
||||
Pattern.compile(".*(Bought|Sold) ([0-9\\.]+) ([A-Za-z]+) @ ([0-9\\.]+)");
|
||||
|
||||
private final Transaction tx;
|
||||
private final Type type;
|
||||
private final int securities;
|
||||
private final String symbol;
|
||||
private final String description;
|
||||
private final double perSecurityPrice;
|
||||
|
||||
public TdAmeritradeStockTransaction(Transaction tx) {
|
||||
this.tx = tx;
|
||||
|
||||
Matcher matcher = this.descriptionFormat.matcher(tx.getDescription());
|
||||
if (!matcher.find())
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
String buysell = matcher.group(1);
|
||||
this.securities = Integer.valueOf(matcher.group(2));
|
||||
this.symbol = matcher.group(3).toUpperCase();
|
||||
this.perSecurityPrice = Double.valueOf(matcher.group(4));
|
||||
|
||||
this.type = this.determineTransactionType(buysell);
|
||||
|
||||
String action = Type.Buy.equals(type) ? "Bought" : "Sold";
|
||||
this.description = new StringBuilder()
|
||||
.append(action).append(' ')
|
||||
.append(NumberFormatFactory.getSecuritiesFormatter().format(this.securities)).append(' ')
|
||||
.append(this.symbol).append(" @ ")
|
||||
.append(NumberFormatFactory.getPriceFormatter().format(this.perSecurityPrice)).toString();
|
||||
}
|
||||
|
||||
private Type determineTransactionType(String buysell) {
|
||||
buysell = buysell.toLowerCase();
|
||||
if (buysell.charAt(0) == 'b') {
|
||||
return Type.Buy;
|
||||
} else if (buysell.charAt(0) == 's') {
|
||||
return Type.Sell;
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transaction getTransaction() {
|
||||
return this.tx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNormalizedDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getSecurities() {
|
||||
return (double)this.securities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSecuritySymbol() {
|
||||
return this.symbol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getPricePerSecurity() {
|
||||
return this.perSecurityPrice;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
|
||||
public class TransactionWrapper {
|
||||
|
||||
private final ParsedTransaction ptx;
|
||||
private final PartiallyReconciledTransaction prtx;
|
||||
private double outstandingSecurities;
|
||||
private double outstandingValue = 0.0;
|
||||
private float splitRatio = 1f;
|
||||
|
||||
public TransactionWrapper(ParsedTransaction ptx) {
|
||||
this.ptx = ptx;
|
||||
|
||||
if (ptx instanceof PartiallyReconciledTransaction) {
|
||||
this.prtx = (PartiallyReconciledTransaction)ptx;
|
||||
if (this.prtx.getUnsoldSecurities() != null)
|
||||
this.outstandingSecurities = this.prtx.getUnsoldSecurities().doubleValue();
|
||||
} else {
|
||||
this.prtx = null;
|
||||
}
|
||||
|
||||
this.outstandingValue = ptx.getTransaction().getAmount();
|
||||
if (this.outstandingSecurities == 0.0) {
|
||||
this.outstandingSecurities = ptx.getSecurities();
|
||||
} else {
|
||||
double frac = this.outstandingSecurities / ptx.getSecurities();
|
||||
this.outstandingValue = Math.round(this.outstandingValue * frac * 10000L) / 10000.0;
|
||||
}
|
||||
}
|
||||
|
||||
public ParsedTransaction getParsedTransaction() {
|
||||
return this.ptx;
|
||||
}
|
||||
|
||||
public Transaction getTransaction() {
|
||||
return this.ptx.getTransaction();
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
if (this.outstandingSecurities == this.getParsedTransaction().getSecurities() || this.outstandingSecurities == 0.0) {
|
||||
return this.ptx.getNormalizedDescription();
|
||||
} else {
|
||||
double outstandingSecurities = this.outstandingSecurities / this.splitRatio;
|
||||
if (outstandingSecurities == this.getParsedTransaction().getSecurities()) {
|
||||
return this.ptx.getNormalizedDescription();
|
||||
} else {
|
||||
return this.ptx.getNormalizedDescription() + "; " + NumberFormatFactory.getSecuritiesFormatter().format(outstandingSecurities) + " unsold";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double getSplitAdjustedSecurities() {
|
||||
if (this.splitRatio == 1f) {
|
||||
return this.ptx.getSecurities();
|
||||
} else {
|
||||
return Math.round(this.ptx.getSecurities() * this.splitRatio);
|
||||
}
|
||||
}
|
||||
|
||||
public double getOutstandingSecurities() {
|
||||
return this.outstandingSecurities;
|
||||
}
|
||||
|
||||
public double getOutstandingValue() {
|
||||
return this.outstandingValue;
|
||||
}
|
||||
|
||||
public double decrementOutstandingSecurities(double securities) {
|
||||
if (this.outstandingSecurities < securities)
|
||||
throw new IllegalStateException();
|
||||
|
||||
this.outstandingSecurities -= securities;
|
||||
if (this.outstandingSecurities == 0) {
|
||||
double value = this.outstandingValue;
|
||||
this.outstandingValue = 0.0;
|
||||
return value;
|
||||
}
|
||||
|
||||
double frac = 1.0;
|
||||
double value = this.ptx.getTransaction().getAmount();
|
||||
if (securities < this.getSplitAdjustedSecurities()) {
|
||||
frac = 1.0 * securities / this.getSplitAdjustedSecurities();
|
||||
value *= frac;
|
||||
}
|
||||
|
||||
this.outstandingValue -= value;
|
||||
return value;
|
||||
}
|
||||
|
||||
public void recordSplit(float ratio) {
|
||||
this.splitRatio *= ratio;
|
||||
this.outstandingSecurities *= ratio;
|
||||
}
|
||||
|
||||
}
|
152
buxfer-cli/src/main/java/com/inteligr8/buxfer/WriterCLI.java
Normal file
152
buxfer-cli/src/main/java/com/inteligr8/buxfer/WriterCLI.java
Normal file
@ -0,0 +1,152 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.cli.CommandLine;
|
||||
import org.apache.commons.cli.DefaultParser;
|
||||
import org.apache.commons.cli.Option;
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.apache.commons.cli.ParseException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.inteligr8.buxfer.api.CommandApi;
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.TransactionsResponse;
|
||||
|
||||
public class WriterCLI {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WriterCLI.class);
|
||||
|
||||
private enum Format {
|
||||
|
||||
Json,
|
||||
Csv,
|
||||
MyStockPortfolioCsv("my-stock-portfolio-csv");
|
||||
|
||||
private final String key;
|
||||
private Format() {
|
||||
this.key = this.toString().toLowerCase();
|
||||
}
|
||||
|
||||
private Format(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public static Format valueByKey(String key) {
|
||||
for (Format format : Format.values())
|
||||
if (format.key.equals(key))
|
||||
return format;
|
||||
return Format.valueOf(key);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws ParseException, IOException, InterruptedException {
|
||||
Options options = buildOptions();
|
||||
String extraCommand = "[options] \"accountName\"";
|
||||
List<String> extraLines = Arrays.asList("accountName: A Buxfer account name");
|
||||
|
||||
CommandLine cli = new DefaultParser().parse(options, args);
|
||||
if (cli.getArgList().isEmpty()) {
|
||||
CLI.help(options, extraCommand, extraLines, "An account name is required");
|
||||
return;
|
||||
} else if (cli.hasOption("help")) {
|
||||
CLI.help(options, extraCommand, extraLines);
|
||||
return;
|
||||
}
|
||||
|
||||
String buxferAccountName = determineBuxferAccountName(cli);
|
||||
CommandApi api = findBuxferApi(cli);
|
||||
|
||||
File file = determineFile(cli);
|
||||
FileOutputStream fostream = new FileOutputStream(file);
|
||||
BufferedOutputStream bostream = new BufferedOutputStream(fostream);
|
||||
try {
|
||||
Observer<Transaction> observer = determineTransactionWriter(cli, bostream);
|
||||
|
||||
searchTransactions(api, buxferAccountName, observer);
|
||||
} finally {
|
||||
bostream.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchTransactions(CommandApi api, String buxferAccountName, Observer<Transaction> observer) throws IOException, InterruptedException {
|
||||
long total = -1L;
|
||||
int pages = 1;
|
||||
for (int p = 1; p <= pages; p++) {
|
||||
TransactionsResponse response = api.getTransactionsInAccount(buxferAccountName, null, null, null, 1).getResponse();
|
||||
if (total < 0L) {
|
||||
total = response.getTotalItems();
|
||||
pages = (int)(response.getTotalItems() / 100L);
|
||||
} else if (total != response.getTotalItems()) {
|
||||
logger.error("Transaction count changed while processing");
|
||||
return;
|
||||
}
|
||||
|
||||
for (Transaction tx : response.getItems())
|
||||
observer.observed(tx);
|
||||
}
|
||||
}
|
||||
|
||||
private static Options buildOptions() {
|
||||
return new Options()
|
||||
.addOption(new Option("u", "email", true, "A Buxfer email for authentication"))
|
||||
.addOption(new Option("p", "password", true, "A Buxfer password for authentication"))
|
||||
.addOption(new Option("t", "format", true, "The output format [json | csv | my-stock-portfolio-csv]"))
|
||||
.addOption(new Option("f", "file", true, "The output filename"))
|
||||
.addOption(new Option(null, "portfolio-name", true, "The portfolio name where applicable"));
|
||||
}
|
||||
|
||||
private static String determineBuxferAccountName(CommandLine cli) {
|
||||
return cli.getArgList().iterator().next();
|
||||
}
|
||||
|
||||
private static File determineFile(CommandLine cli) {
|
||||
Format format = Format.valueByKey(cli.getOptionValue("format", "json"));
|
||||
|
||||
String filename = cli.hasOption("file") ? cli.getOptionValue("file") : "portfolio";
|
||||
if (filename.lastIndexOf('.') < 0) {
|
||||
switch (format) {
|
||||
case Json:
|
||||
filename += ".json";
|
||||
break;
|
||||
default:
|
||||
filename += ".csv";
|
||||
}
|
||||
}
|
||||
return new File(filename);
|
||||
}
|
||||
|
||||
private static Observer<Transaction> determineTransactionWriter(CommandLine cli, OutputStream ostream) {
|
||||
Format format = Format.valueByKey(cli.getOptionValue("format", "json"));
|
||||
|
||||
switch (format) {
|
||||
case Json:
|
||||
return new JsonTransactionWriter(ostream);
|
||||
case MyStockPortfolioCsv:
|
||||
return new MyStockPortfolioCsvTransactionWriter(ostream);
|
||||
default:
|
||||
return new CsvTransactionWriter(ostream);
|
||||
}
|
||||
}
|
||||
|
||||
private static CommandApi findBuxferApi(CommandLine cli) {
|
||||
BuxferClientConfiguration config = new BuxferClientConfiguration();
|
||||
config.setBaseUrl("https://www.buxfer.com");
|
||||
|
||||
if (cli.hasOption("email"))
|
||||
config.setAuthEmail(cli.getOptionValue("email"));
|
||||
if (cli.hasOption("password"))
|
||||
config.setAuthPassword(cli.getOptionValue("password"));
|
||||
|
||||
BuxferClientJerseyImpl client = new BuxferClientJerseyImpl(config);
|
||||
return client.getApi().getCommandApi();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.impl.client.DefaultRedirectStrategy;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
|
||||
import com.inteligr8.rs.ClientConfiguration;
|
||||
|
||||
public abstract class ConditionalIT {
|
||||
|
||||
public abstract ClientConfiguration getConfiguration();
|
||||
|
||||
public boolean hostExists() {
|
||||
String uri = this.getConfiguration().getBaseUrl();
|
||||
|
||||
HttpUriRequest request = RequestBuilder.get()
|
||||
.setUri(uri)
|
||||
.build();
|
||||
|
||||
HttpClient client = HttpClientBuilder.create()
|
||||
.setRedirectStrategy(DefaultRedirectStrategy.INSTANCE)
|
||||
.build();
|
||||
|
||||
try {
|
||||
HttpResponse response = client.execute(request);
|
||||
return response.getStatusLine().getStatusCode() < 300;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
package com.inteligr8.buxfer;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
|
||||
import com.inteligr8.buxfer.api.CommandApi;
|
||||
import com.inteligr8.buxfer.model.Account;
|
||||
import com.inteligr8.buxfer.model.ArrayResponse;
|
||||
import com.inteligr8.buxfer.model.BaseResponse;
|
||||
import com.inteligr8.buxfer.model.BaseResponse.Status;
|
||||
import com.inteligr8.buxfer.model.Budget;
|
||||
import com.inteligr8.buxfer.model.Item;
|
||||
import com.inteligr8.buxfer.model.NamedItem;
|
||||
import com.inteligr8.buxfer.model.Reminder;
|
||||
import com.inteligr8.buxfer.model.Tag;
|
||||
import com.inteligr8.buxfer.model.Transaction;
|
||||
import com.inteligr8.buxfer.model.TransactionsResponse;
|
||||
|
||||
public abstract class ConnectionClientIT extends ConditionalIT {
|
||||
|
||||
public abstract BuxferPublicRestApi getClient();
|
||||
|
||||
private CommandApi api;
|
||||
|
||||
public boolean fullTest() {
|
||||
return false;
|
||||
//return this.hostExists();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void getApi() {
|
||||
this.api = this.getClient().getCommandApi();
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("hostExists")
|
||||
public void testTransactions() {
|
||||
TransactionsResponse response = this.api.getTransactions(
|
||||
LocalDate.of(2021, 12, 1),
|
||||
LocalDate.of(2021, 12, 31),
|
||||
null,
|
||||
1
|
||||
).getResponse();
|
||||
this.assertArrayNotEmpty(response);
|
||||
Assertions.assertTrue(response.getTotalItems() > 0L);
|
||||
this.assertItems(response.getItems());
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("hostExists")
|
||||
public void testAccounts() {
|
||||
ArrayResponse<? extends Item> response = this.api.getAccounts().getResponse();
|
||||
this.assertArrayNotEmpty(response);
|
||||
this.assertItems(response.getItems());
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("hostExists")
|
||||
public void testTags() {
|
||||
ArrayResponse<? extends Item> response = this.api.getTags().getResponse();
|
||||
this.assertArrayNotEmpty(response);
|
||||
this.assertItems(response.getItems());
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("hostExists")
|
||||
public void testBudgets() {
|
||||
ArrayResponse<? extends Item> response = this.api.getBudgets().getResponse();
|
||||
this.assertArrayNotEmpty(response);
|
||||
this.assertItems(response.getItems());
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("hostExists")
|
||||
public void testReminders() {
|
||||
ArrayResponse<? extends Item> response = this.api.getReminders().getResponse();
|
||||
this.assertArrayNotEmpty(response);
|
||||
this.assertItems(response.getItems());
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("fullTest")
|
||||
public void testGroups() {
|
||||
ArrayResponse<? extends Item> response = this.api.getGroups().getResponse();
|
||||
this.assertArrayEmpty(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("fullTest")
|
||||
public void testContacts() {
|
||||
ArrayResponse<? extends Item> response = this.api.getContacts().getResponse();
|
||||
this.assertArrayEmpty(response);
|
||||
}
|
||||
|
||||
private void assertOk(BaseResponse response) {
|
||||
Assertions.assertNotNull(response);
|
||||
Assertions.assertEquals(Status.OK, response.getStatus());
|
||||
Assertions.assertNull(response.getError());
|
||||
}
|
||||
|
||||
private void assertArrayEmpty(ArrayResponse<?> response) {
|
||||
this.assertOk(response);
|
||||
Assertions.assertNotNull(response.getItems());
|
||||
Assertions.assertTrue(response.getItems().isEmpty());
|
||||
}
|
||||
|
||||
private void assertArrayNotEmpty(ArrayResponse<?> response) {
|
||||
this.assertOk(response);
|
||||
Assertions.assertNotNull(response.getItems());
|
||||
Assertions.assertFalse(response.getItems().isEmpty());
|
||||
}
|
||||
|
||||
private void assertItem(Item item) {
|
||||
Assertions.assertTrue(item.getId() > 0);
|
||||
}
|
||||
|
||||
private void assertNamedItem(NamedItem item) {
|
||||
this.assertItem(item);
|
||||
Assertions.assertNotNull(item.getName());
|
||||
Assertions.assertNotEquals(0, item.getName().length());
|
||||
}
|
||||
|
||||
private void assertSpecificItem(Item item) {
|
||||
if (item instanceof Transaction) {
|
||||
this.assertTransaction((Transaction)item);
|
||||
} else if (item instanceof Budget) {
|
||||
this.assertBudget((Budget)item);
|
||||
} else if (item instanceof Reminder) {
|
||||
this.assertReminder((Reminder)item);
|
||||
} else if (item instanceof Tag) {
|
||||
this.assertTag((Tag)item);
|
||||
} else if (item instanceof Account) {
|
||||
this.assertAccount((Account)item);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertTransaction(Transaction item) {
|
||||
this.assertItem(item);
|
||||
Assertions.assertNotNull(item.getType());
|
||||
Assertions.assertNotNull(item.getAmount());
|
||||
Assertions.assertNotNull(item.getDate());
|
||||
}
|
||||
|
||||
private void assertAccount(Account item) {
|
||||
this.assertNamedItem(item);
|
||||
Assertions.assertNotNull(item.getBank());
|
||||
Assertions.assertNotEquals(0, item.getBank().length());
|
||||
Assertions.assertNotNull(item.getBalance());
|
||||
}
|
||||
|
||||
private void assertTag(Tag item) {
|
||||
this.assertNamedItem(item);
|
||||
}
|
||||
|
||||
private void assertBudget(Budget item) {
|
||||
this.assertNamedItem(item);
|
||||
}
|
||||
|
||||
private void assertReminder(Reminder item) {
|
||||
this.assertNamedItem(item);
|
||||
Assertions.assertNotNull(item.getStartDate());
|
||||
Assertions.assertTrue(item.getStartDate().isAfter(LocalDate.of(2010, 1, 1)));
|
||||
}
|
||||
|
||||
private void assertItems(Collection<? extends Item> items) {
|
||||
for (Item item : items) {
|
||||
this.assertSpecificItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
19
buxfer-cli/src/test/resources/log4j2.xml
Normal file
19
buxfer-cli/src/test/resources/log4j2.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="com.inteligr8.buxfer" level="debug" additivity="false">
|
||||
<AppenderRef ref="Console" />
|
||||
</Logger>
|
||||
<Logger name="jaxrs.request" level="trace" additivity="false">
|
||||
<AppenderRef ref="Console" />
|
||||
</Logger>
|
||||
<Root level="warn">
|
||||
<AppenderRef ref="Console" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
@ -2,16 +2,15 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.inteligr8.buxfer</groupId>
|
||||
<artifactId>buxfer-public-rest-api</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<name>Buxfer ReST API for Java</name>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
<parent>
|
||||
<groupId>com.inteligr8.buxfer</groupId>
|
||||
<artifactId>buxfer-public-rest-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@ -43,22 +42,4 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>inteligr8-public</id>
|
||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>inteligr8-releases</id>
|
||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>inteligr8-snapshots</id>
|
||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-snapshots</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
</project>
|
||||
|
@ -3,10 +3,14 @@ package com.inteligr8.buxfer.api;
|
||||
import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.FormParam;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import com.inteligr8.buxfer.model.AccountsResponse;
|
||||
import com.inteligr8.buxfer.model.BudgetsResponse;
|
||||
@ -17,6 +21,7 @@ import com.inteligr8.buxfer.model.RemindersResponse;
|
||||
import com.inteligr8.buxfer.model.Response;
|
||||
import com.inteligr8.buxfer.model.TagsResponse;
|
||||
import com.inteligr8.buxfer.model.Transaction.Status;
|
||||
import com.inteligr8.buxfer.model.TransactionResponse;
|
||||
import com.inteligr8.buxfer.model.TransactionsResponse;
|
||||
|
||||
@Path("/api")
|
||||
@ -24,7 +29,7 @@ public interface CommandApi {
|
||||
|
||||
@GET
|
||||
@Path("/transactions")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionsResponse> getTransactions(
|
||||
@QueryParam("startDate") LocalDate startDate,
|
||||
@QueryParam("endDate") LocalDate endDate,
|
||||
@ -33,7 +38,7 @@ public interface CommandApi {
|
||||
|
||||
@GET
|
||||
@Path("/transactions")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionsResponse> getTransactionsInTag(
|
||||
@QueryParam("tagName") String tagName,
|
||||
@QueryParam("startDate") LocalDate startDate,
|
||||
@ -43,7 +48,7 @@ public interface CommandApi {
|
||||
|
||||
@GET
|
||||
@Path("/transactions")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionsResponse> getTransactionsInAccount(
|
||||
@QueryParam("accountName") String accountName,
|
||||
@QueryParam("startDate") LocalDate startDate,
|
||||
@ -53,7 +58,7 @@ public interface CommandApi {
|
||||
|
||||
@GET
|
||||
@Path("/transactions")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionsResponse> getTransactions(
|
||||
@QueryParam("accountName") String accountName,
|
||||
@QueryParam("tagName") String tagName,
|
||||
@ -67,7 +72,7 @@ public interface CommandApi {
|
||||
|
||||
@GET
|
||||
@Path("/transactions")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionsResponse> getTransactions(
|
||||
@QueryParam("accountName") String accountName,
|
||||
@QueryParam("tagName") String tagName,
|
||||
@ -80,108 +85,184 @@ public interface CommandApi {
|
||||
|
||||
@GET
|
||||
@Path("/transactions")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionsResponse> getTransactionsByIds(
|
||||
@QueryParam("accountId") String accountId,
|
||||
@QueryParam("tagId") String tagId,
|
||||
@QueryParam("accountId") Long accountId,
|
||||
@QueryParam("tagId") Long tagId,
|
||||
@QueryParam("startDate") LocalDate startDate,
|
||||
@QueryParam("endDate") LocalDate endDate,
|
||||
@QueryParam("budgetId") String budgetId,
|
||||
@QueryParam("contactId") String contactId,
|
||||
@QueryParam("groupId") String groupId,
|
||||
@QueryParam("budgetId") Long budgetId,
|
||||
@QueryParam("contactId") Long contactId,
|
||||
@QueryParam("groupId") Long groupId,
|
||||
@QueryParam("status") Status status,
|
||||
@QueryParam("page") int page);
|
||||
|
||||
@GET
|
||||
@Path("/transactions")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionsResponse> getTransactionsByIds(
|
||||
@QueryParam("accountId") String accountId,
|
||||
@QueryParam("tagId") String tagId,
|
||||
@QueryParam("accountId") Long accountId,
|
||||
@QueryParam("tagId") Long tagId,
|
||||
@QueryParam("startDate") YearMonth month,
|
||||
@QueryParam("budgetId") String budgetId,
|
||||
@QueryParam("contactId") String contactId,
|
||||
@QueryParam("groupId") String groupId,
|
||||
@QueryParam("budgetId") Long budgetId,
|
||||
@QueryParam("contactId") Long contactId,
|
||||
@QueryParam("groupId") Long groupId,
|
||||
@QueryParam("status") Status status,
|
||||
@QueryParam("page") int page);
|
||||
|
||||
@GET
|
||||
@Path("/transactions")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionsResponse> getTransactions(
|
||||
@QueryParam("accountId") String accountId,
|
||||
@QueryParam("accountId") Long accountId,
|
||||
@QueryParam("accountName") String accountName,
|
||||
@QueryParam("tagId") String tagId,
|
||||
@QueryParam("tagId") Long tagId,
|
||||
@QueryParam("tagName") String tagName,
|
||||
@QueryParam("startDate") LocalDate startDate,
|
||||
@QueryParam("endDate") LocalDate endDate,
|
||||
@QueryParam("budgetId") String budgetId,
|
||||
@QueryParam("budgetId") Long budgetId,
|
||||
@QueryParam("budgetName") String budgetName,
|
||||
@QueryParam("contactId") String contactId,
|
||||
@QueryParam("contactId") Long contactId,
|
||||
@QueryParam("contactName") String contactName,
|
||||
@QueryParam("groupId") String groupId,
|
||||
@QueryParam("groupId") Long groupId,
|
||||
@QueryParam("groupName") String groupName,
|
||||
@QueryParam("status") Status status,
|
||||
@QueryParam("page") int page);
|
||||
|
||||
@GET
|
||||
@Path("/transactions")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionsResponse> getTransactions(
|
||||
@QueryParam("accountId") String accountId,
|
||||
@QueryParam("accountId") Long accountId,
|
||||
@QueryParam("accountName") String accountName,
|
||||
@QueryParam("tagId") String tagId,
|
||||
@QueryParam("tagId") Long tagId,
|
||||
@QueryParam("tagName") String tagName,
|
||||
@QueryParam("month") YearMonth month,
|
||||
@QueryParam("budgetId") String budgetId,
|
||||
@QueryParam("budgetId") Long budgetId,
|
||||
@QueryParam("budgetName") String budgetName,
|
||||
@QueryParam("contactId") String contactId,
|
||||
@QueryParam("contactId") Long contactId,
|
||||
@QueryParam("contactName") String contactName,
|
||||
@QueryParam("groupId") String groupId,
|
||||
@QueryParam("groupId") Long groupId,
|
||||
@QueryParam("groupName") String groupName,
|
||||
@QueryParam("status") Status status,
|
||||
@QueryParam("page") int page);
|
||||
|
||||
@GET
|
||||
@Path("/transactions")
|
||||
@Produces({ "application/json" })
|
||||
public String getJson(
|
||||
@QueryParam("accountName") String accountName,
|
||||
@QueryParam("page") int page);
|
||||
@POST
|
||||
@Path("/transaction_add")
|
||||
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionResponse> addTransaction(
|
||||
@FormParam("type") String type,
|
||||
@FormParam("accountId") long accountId,
|
||||
@FormParam("date") LocalDate date,
|
||||
@FormParam("description") String description,
|
||||
@FormParam("amount") double amount,
|
||||
@FormParam("tags") String tags,
|
||||
@FormParam("status") Status status);
|
||||
|
||||
@POST
|
||||
@Path("/transaction_add")
|
||||
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionResponse> addTransaction(
|
||||
@FormParam("type") String type,
|
||||
@FormParam("fromAccountId") Long fromAccountId,
|
||||
@FormParam("toAccountId") Long toAccountId,
|
||||
@FormParam("date") LocalDate date,
|
||||
@FormParam("description") String description,
|
||||
@FormParam("amount") double amount,
|
||||
@FormParam("tags") String tags,
|
||||
@FormParam("status") Status status);
|
||||
|
||||
@POST
|
||||
@Path("/transaction_edit")
|
||||
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionResponse> editTransaction(
|
||||
@FormParam("id") long id,
|
||||
@FormParam("description") String description,
|
||||
@FormParam("tags") String tags,
|
||||
@FormParam("status") Status status);
|
||||
|
||||
@POST
|
||||
@Path("/transaction_edit")
|
||||
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionResponse> editTransaction(
|
||||
@FormParam("id") long id,
|
||||
@FormParam("type") String type,
|
||||
@FormParam("description") String description,
|
||||
@FormParam("tags") String tags,
|
||||
@FormParam("status") Status status);
|
||||
|
||||
@POST
|
||||
@Path("/transaction_edit")
|
||||
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionResponse> editTransaction(
|
||||
@FormParam("id") long id,
|
||||
@FormParam("type") String type,
|
||||
@FormParam("accountId") Long accountId,
|
||||
@FormParam("date") LocalDate date,
|
||||
@FormParam("description") String description,
|
||||
@FormParam("amount") Double amount,
|
||||
@FormParam("tags") String tags,
|
||||
@FormParam("status") Status status);
|
||||
|
||||
@POST
|
||||
@Path("/transaction_edit")
|
||||
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TransactionResponse> editTransaction(
|
||||
@FormParam("id") long id,
|
||||
@FormParam("type") String type,
|
||||
@FormParam("fromAccountId") Long fromAccountId,
|
||||
@FormParam("toAccountId") Long toAccountId,
|
||||
@FormParam("date") LocalDate date,
|
||||
@FormParam("description") String description,
|
||||
@FormParam("amount") Double amount,
|
||||
@FormParam("tags") String tags,
|
||||
@FormParam("status") Status status);
|
||||
|
||||
@POST
|
||||
@Path("/transaction_delete")
|
||||
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
|
||||
public void deleteTransaction(
|
||||
@FormParam("id") long id);
|
||||
|
||||
@GET
|
||||
@Path("/accounts")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<AccountsResponse> getAccounts();
|
||||
|
||||
@GET
|
||||
@Path("/loans")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<LoansResponse> getLoans();
|
||||
|
||||
@GET
|
||||
@Path("/tags")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<TagsResponse> getTags();
|
||||
|
||||
@GET
|
||||
@Path("/budgets")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<BudgetsResponse> getBudgets();
|
||||
|
||||
@GET
|
||||
@Path("/reminders")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<RemindersResponse> getReminders();
|
||||
|
||||
@GET
|
||||
@Path("/groups")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<GroupsResponse> getGroups();
|
||||
|
||||
@GET
|
||||
@Path("/contacts")
|
||||
@Produces({ "application/json" })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public Response<ContactsResponse> getContacts();
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
package com.inteligr8.buxfer.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ItemResponse extends BaseResponse {
|
||||
|
||||
@JsonProperty
|
||||
private String id;
|
||||
|
||||
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
}
|
@ -1,11 +1,25 @@
|
||||
package com.inteligr8.buxfer.model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
/**
|
||||
* {
|
||||
@ -47,7 +61,66 @@ public class Transaction extends Item {
|
||||
Cleared
|
||||
}
|
||||
|
||||
private static final Map<String, Type> incomingTypeMap = new HashMap<>(20);
|
||||
|
||||
@JsonDeserialize(using = Type.Deserializer.class)
|
||||
@JsonSerialize(using = Type.Serializer.class)
|
||||
public enum Type {
|
||||
Income("income"),
|
||||
Expense("expense"),
|
||||
Refund("refund"),
|
||||
Transfer("transfer"),
|
||||
Buy("investment purchase", "investment_buy"),
|
||||
Sell("investment sale", "investment_sell"),
|
||||
Dividend("dividend", "investment_dividend"),
|
||||
CapitalGain("capital gain", "capital_gain"),
|
||||
SharedBill("sharedBill"),
|
||||
PaidForFriend("paidForFriend"),
|
||||
Loan("loan"),
|
||||
Settlement("settlement");
|
||||
|
||||
private final String outgoingValue;
|
||||
|
||||
private Type(String value) {
|
||||
this(value.toLowerCase(), value);
|
||||
}
|
||||
|
||||
private Type(String incomingValue, String outgoingValue) {
|
||||
this.outgoingValue = outgoingValue;
|
||||
incomingTypeMap.put(incomingValue, this);
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getOutgoingValue() {
|
||||
return this.outgoingValue;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static Type fromIncomingValue(String value) {
|
||||
return incomingTypeMap.get(value);
|
||||
}
|
||||
|
||||
// FIXME the serializer is not getting called when expected; but deserializer is getting called and works
|
||||
public static class Serializer extends JsonSerializer<Type> {
|
||||
public Serializer() {}
|
||||
@Override
|
||||
public void serialize(Type value, JsonGenerator gen, SerializerProvider provider) throws IOException {
|
||||
gen.writeString(value.outgoingValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonDeserializer<Type> {
|
||||
public Deserializer() {}
|
||||
@Override
|
||||
public Type deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
|
||||
String value = p.getText();
|
||||
return Type.fromIncomingValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum IncomingType {
|
||||
@JsonProperty("income")
|
||||
Income,
|
||||
@JsonProperty("expense")
|
||||
@ -62,12 +135,51 @@ public class Transaction extends Item {
|
||||
Sell,
|
||||
@JsonProperty("dividend")
|
||||
Dividend,
|
||||
@JsonProperty("capital gain")
|
||||
CapitalGain,
|
||||
@JsonProperty("sharedbill")
|
||||
SharedBill,
|
||||
@JsonProperty("paidforfriend")
|
||||
PaidForFriend,
|
||||
@JsonProperty("loan")
|
||||
Loan
|
||||
Loan,
|
||||
@JsonProperty("settlement")
|
||||
Settlement;
|
||||
|
||||
public OutgoingType toOutgoingType() {
|
||||
return OutgoingType.valueOf(this.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public enum OutgoingType {
|
||||
@JsonProperty("income")
|
||||
Income,
|
||||
@JsonProperty("expense")
|
||||
Expense,
|
||||
@JsonProperty("refund")
|
||||
Refund,
|
||||
@JsonProperty("transfer")
|
||||
Transfer,
|
||||
@JsonProperty("investment_buy")
|
||||
Buy,
|
||||
@JsonProperty("investment_sell")
|
||||
Sell,
|
||||
@JsonProperty("investment_dividend")
|
||||
Dividend,
|
||||
@JsonProperty("capital_gain")
|
||||
CapitalGain,
|
||||
@JsonProperty("sharedBill")
|
||||
SharedBill,
|
||||
@JsonProperty("paidForFriend")
|
||||
PaidForFriend,
|
||||
@JsonProperty("loan")
|
||||
Loan,
|
||||
@JsonProperty("settlement")
|
||||
Settlement;
|
||||
|
||||
public IncomingType toIncomingType() {
|
||||
return IncomingType.valueOf(this.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
|
@ -0,0 +1,22 @@
|
||||
package com.inteligr8.buxfer.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class TransactionResponse {
|
||||
|
||||
@JsonProperty
|
||||
private String id;
|
||||
|
||||
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
}
|
@ -2,19 +2,19 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.inteligr8.buxfer</groupId>
|
||||
<artifactId>buxfer-public-rest-client</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<name>Buxfer ReST Client for Java</name>
|
||||
|
||||
<parent>
|
||||
<groupId>com.inteligr8.buxfer</groupId>
|
||||
<artifactId>buxfer-public-rest-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
|
||||
<jaxrs.impl>jersey</jaxrs.impl>
|
||||
|
||||
<junit.version>5.7.2</junit.version>
|
||||
<spring.version>5.2.14.RELEASE</spring.version>
|
||||
|
||||
<api.model.disabled>false</api.model.disabled>
|
||||
@ -27,9 +27,9 @@
|
||||
<version>1.1-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.inteligr8.buxfer</groupId>
|
||||
<groupId>${project.parent.groupId}</groupId>
|
||||
<artifactId>buxfer-public-rest-api</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
@ -39,7 +39,6 @@
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -89,28 +88,6 @@
|
||||
<classifier>${jaxrs.impl}</classifier>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
@ -126,31 +103,26 @@
|
||||
</activation>
|
||||
<properties>
|
||||
<jaxrs.impl>jersey</jaxrs.impl>
|
||||
<jersey.version>2.34</jersey.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.core</groupId>
|
||||
<artifactId>jersey-client</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.ext</groupId>
|
||||
<artifactId>jersey-proxy-client</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.inject</groupId>
|
||||
<artifactId>jersey-hk2</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.media</groupId>
|
||||
<artifactId>jersey-media-json-jackson</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
@ -165,41 +137,14 @@
|
||||
</activation>
|
||||
<properties>
|
||||
<jaxrs.impl>cxf</jaxrs.impl>
|
||||
<cxf.version>3.3.2</cxf.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.cxf</groupId>
|
||||
<artifactId>cxf-rt-rs-client</artifactId>
|
||||
<version>${cxf.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>inteligr8-public</id>
|
||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>inteligr8-public</id>
|
||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>inteligr8-releases</id>
|
||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>inteligr8-snapshots</id>
|
||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-snapshots</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
</project>
|
||||
|
@ -2,6 +2,7 @@ package com.inteligr8.buxfer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.ws.rs.NotAuthorizedException;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.client.ClientRequestContext;
|
||||
import javax.ws.rs.client.Entity;
|
||||
@ -50,16 +51,22 @@ public class BuxferAuthorizationFilter implements AuthorizationFilter {
|
||||
|
||||
GenericType<Response<TokenResponse>> responseType = new GenericType<Response<TokenResponse>>() {};
|
||||
|
||||
TokenResponse response = requestContext.getClient()
|
||||
.target(loginUri)
|
||||
.request()
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.post(Entity.form(form), responseType)
|
||||
.getResponse();
|
||||
|
||||
if (!com.inteligr8.buxfer.model.BaseResponse.Status.OK.equals(response.getStatus()))
|
||||
throw new WebApplicationException(response.getError(), Status.UNAUTHORIZED.getStatusCode());
|
||||
this.token = response.getToken();
|
||||
try {
|
||||
TokenResponse response = requestContext.getClient()
|
||||
.target(loginUri)
|
||||
.request()
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.post(Entity.form(form), responseType)
|
||||
.getResponse();
|
||||
|
||||
if (!com.inteligr8.buxfer.model.BaseResponse.Status.OK.equals(response.getStatus()))
|
||||
throw new WebApplicationException(response.getError(), Status.UNAUTHORIZED.getStatusCode());
|
||||
this.token = response.getToken();
|
||||
} catch (NotAuthorizedException nae) {
|
||||
throw nae;
|
||||
} catch (WebApplicationException wae) {
|
||||
throw new NotAuthorizedException("Indirect due to non-authorization failure: " + wae.getMessage(), wae);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import com.inteligr8.rs.ClientJerseyConfiguration;
|
||||
@ComponentScan
|
||||
public class BuxferClientConfiguration implements ClientCxfConfiguration, ClientJerseyConfiguration {
|
||||
|
||||
@Value("${buxfer.service.baseUrl:http://localhost:8080/alfresco}")
|
||||
@Value("${buxfer.service.baseUrl:https://www.buxfer.com}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${buxfer.service.security.auth.email}")
|
||||
|
136
pom.xml
Normal file
136
pom.xml
Normal file
@ -0,0 +1,136 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.inteligr8.buxfer</groupId>
|
||||
<artifactId>buxfer-public-rest-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Buxfer Public ReST Projects</name>
|
||||
|
||||
<scm>
|
||||
<url>https://bitbucket.org/inteligr8/buxfer-public-rest</url>
|
||||
</scm>
|
||||
<organization>
|
||||
<name>Inteligr8</name>
|
||||
<url>https://www.inteligr8.com</url>
|
||||
</organization>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>Brian Long</name>
|
||||
<email>brian@inteligr8.com</email>
|
||||
<url>https://twitter.com/brianmlong</url>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
|
||||
<log4j.version>2.17.0</log4j.version>
|
||||
<jersey.version>2.35</jersey.version>
|
||||
<cxf.version>3.3.2</cxf.version>
|
||||
<junit.version>5.8.2</junit.version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>buxfer-public-rest-api</module>
|
||||
<module>buxfer-public-rest-client</module>
|
||||
<module>buxfer-cli</module>
|
||||
</modules>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j-impl</artifactId>
|
||||
<version>${log4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.core</groupId>
|
||||
<artifactId>jersey-client</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.ext</groupId>
|
||||
<artifactId>jersey-proxy-client</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.inject</groupId>
|
||||
<artifactId>jersey-hk2</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.media</groupId>
|
||||
<artifactId>jersey-media-json-jackson</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cxf</groupId>
|
||||
<artifactId>cxf-rt-rs-client</artifactId>
|
||||
<version>${cxf.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>inteligr8-public</id>
|
||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>inteligr8-public</id>
|
||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
<distributionManagement>
|
||||
<repository>
|
||||
<id>inteligr8-releases</id>
|
||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-public</url>
|
||||
</repository>
|
||||
<snapshotRepository>
|
||||
<id>inteligr8-snapshots</id>
|
||||
<url>https://repos.inteligr8.com/nexus/repository/inteligr8-snapshots</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
</project>
|
Loading…
x
Reference in New Issue
Block a user