From 8d8ee0016da472255b3407b88c972a1cf55cb6c9 Mon Sep 17 00:00:00 2001 From: Aaron Lindsay Date: Wed, 22 Mar 2017 20:29:08 -0400 Subject: [PATCH] Add command-line client --- cmd/ofx/bankdownload.go | 82 ++++++++++++++ cmd/ofx/banktransactions.go | 93 ++++++++++++++++ cmd/ofx/ccdownload.go | 77 +++++++++++++ cmd/ofx/cctransactions.go | 85 +++++++++++++++ cmd/ofx/command.go | 61 +++++++++++ cmd/ofx/get_accounts.go | 70 ++++++++++++ cmd/ofx/invdownload.go | 87 +++++++++++++++ cmd/ofx/invtransactions.go | 208 ++++++++++++++++++++++++++++++++++++ cmd/ofx/main.go | 80 ++++++++++++++ cmd/ofx/util.go | 24 +++++ 10 files changed, 867 insertions(+) create mode 100644 cmd/ofx/bankdownload.go create mode 100644 cmd/ofx/banktransactions.go create mode 100644 cmd/ofx/ccdownload.go create mode 100644 cmd/ofx/cctransactions.go create mode 100644 cmd/ofx/command.go create mode 100644 cmd/ofx/get_accounts.go create mode 100644 cmd/ofx/invdownload.go create mode 100644 cmd/ofx/invtransactions.go create mode 100644 cmd/ofx/main.go create mode 100644 cmd/ofx/util.go diff --git a/cmd/ofx/bankdownload.go b/cmd/ofx/bankdownload.go new file mode 100644 index 0000000..e5f2588 --- /dev/null +++ b/cmd/ofx/bankdownload.go @@ -0,0 +1,82 @@ +package main + +import ( + "flag" + "fmt" + "github.com/aclindsa/ofxgo" + "io" + "os" + "time" +) + +var downloadCommand = Command{ + Name: "download-bank", + Description: "Download a bank account statement to a file", + Flags: flag.NewFlagSet("download-bank", flag.ExitOnError), + CheckFlags: downloadCheckFlags, + Do: download, +} + +var filename, bankId, acctId, acctType string + +func init() { + defineServerFlags(downloadCommand.Flags) + downloadCommand.Flags.StringVar(&filename, "filename", "./download.ofx", "The file to save to") + downloadCommand.Flags.StringVar(&bankId, "bankid", "", "BankId (from `get-accounts` subcommand)") + downloadCommand.Flags.StringVar(&acctId, "acctid", "", "AcctId (from `get-accounts` subcommand)") + downloadCommand.Flags.StringVar(&acctType, "accttype", "CHECKING", "AcctType (from `get-accounts` subcommand)") +} + +func downloadCheckFlags() bool { + ret := checkServerFlags() + + if len(filename) == 0 { + fmt.Println("Error: Filename empty") + return false + } + + return ret +} + +func download() { + client, query := NewRequest() + + uid, err := ofxgo.RandomUID() + if err != nil { + fmt.Println("Error creating uid for transaction:", err) + os.Exit(1) + } + + statementRequest := ofxgo.StatementRequest{ + TrnUID: *uid, + BankAcctFrom: ofxgo.BankAcct{ + BankId: ofxgo.String(bankId), + AcctId: ofxgo.String(acctId), + AcctType: ofxgo.String(acctType), + }, + DtStart: ofxgo.Date(time.Unix(0, 0)), + DtEnd: ofxgo.Date(time.Now()), + Include: true, + } + query.Banking = append(query.Banking, &statementRequest) + + response, err := client.RequestNoParse(query) + if err != nil { + fmt.Println("Error requesting account statement:", err) + os.Exit(1) + } + defer response.Body.Close() + + file, err := os.Create(filename) + if err != nil { + fmt.Println("Error creating file to write to:", err) + os.Exit(1) + } + defer file.Close() + + _, err = io.Copy(file, response.Body) + if err != nil { + fmt.Println("Error writing response to file:", err) + os.Exit(1) + } +} diff --git a/cmd/ofx/banktransactions.go b/cmd/ofx/banktransactions.go new file mode 100644 index 0000000..f4c01c9 --- /dev/null +++ b/cmd/ofx/banktransactions.go @@ -0,0 +1,93 @@ +package main + +import ( + "flag" + "fmt" + "github.com/aclindsa/ofxgo" + "os" + "time" +) + +var bankTransactionsCommand = Command{ + Name: "transactions-bank", + Description: "Print bank transactions and balance", + Flags: flag.NewFlagSet("transactions-bank", flag.ExitOnError), + CheckFlags: checkServerFlags, + Do: bankTransactions, +} + +func init() { + defineServerFlags(bankTransactionsCommand.Flags) + bankTransactionsCommand.Flags.StringVar(&bankId, "bankid", "", "BankId (from `get-accounts` subcommand)") + bankTransactionsCommand.Flags.StringVar(&acctId, "acctid", "", "AcctId (from `get-accounts` subcommand)") + bankTransactionsCommand.Flags.StringVar(&acctType, "accttype", "CHECKING", "AcctType (from `get-accounts` subcommand)") +} + +func bankTransactions() { + client, query := NewRequest() + + uid, err := ofxgo.RandomUID() + if err != nil { + fmt.Println("Error creating uid for transaction:", err) + os.Exit(1) + } + + statementRequest := ofxgo.StatementRequest{ + TrnUID: *uid, + BankAcctFrom: ofxgo.BankAcct{ + BankId: ofxgo.String(bankId), + AcctId: ofxgo.String(acctId), + AcctType: ofxgo.String(acctType), + }, + DtStart: ofxgo.Date(time.Unix(0, 0)), + DtEnd: ofxgo.Date(time.Now()), + Include: true, + } + query.Banking = append(query.Banking, &statementRequest) + + response, err := client.Request(query) + if err != nil { + fmt.Println("Error requesting account statement:", err) + os.Exit(1) + } + + if response.Signon.Status.Code != 0 { + fmt.Printf("Nonzero signon status with message: %s\n", response.Signon.Status.Message) + os.Exit(1) + } + + if len(response.Banking) < 1 { + fmt.Println("No banking messages received") + return + } + + if stmt, ok := response.Banking[0].(ofxgo.StatementResponse); ok { + fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf) + fmt.Println("Transactions:") + for _, tran := range stmt.BankTranList.Transactions { + printTransaction(stmt.CurDef, &tran) + } + } +} + +func printTransaction(defCurrency ofxgo.String, tran *ofxgo.Transaction) { + currency := defCurrency + if len(tran.Currency) > 0 { + currency = tran.Currency + } else if len(tran.OrigCurrency) > 0 { + currency = tran.Currency + } + + var name string + if len(tran.Name) > 0 { + name = string(tran.Name) + } else { + name = string(tran.Payee.Name) + } + + if len(tran.Memo) > 0 { + name = name + " - " + string(tran.Memo) + } + + fmt.Printf("%s %-15s %-11s %s\n", tran.DtPosted, tran.TrnAmt.String()+" "+string(currency), tran.TrnType, name) +} diff --git a/cmd/ofx/ccdownload.go b/cmd/ofx/ccdownload.go new file mode 100644 index 0000000..247882f --- /dev/null +++ b/cmd/ofx/ccdownload.go @@ -0,0 +1,77 @@ +package main + +import ( + "flag" + "fmt" + "github.com/aclindsa/ofxgo" + "io" + "os" + "time" +) + +var ccDownloadCommand = Command{ + Name: "download-cc", + Description: "Download a credit card account statement to a file", + Flags: flag.NewFlagSet("download-cc", flag.ExitOnError), + CheckFlags: ccDownloadCheckFlags, + Do: ccDownload, +} + +func init() { + defineServerFlags(ccDownloadCommand.Flags) + ccDownloadCommand.Flags.StringVar(&filename, "filename", "./download.ofx", "The file to save to") + ccDownloadCommand.Flags.StringVar(&acctId, "acctid", "", "AcctId (from `get-accounts` subcommand)") +} + +func ccDownloadCheckFlags() bool { + ret := checkServerFlags() + + if len(filename) == 0 { + fmt.Println("Error: Filename empty") + return false + } + + return ret +} + +func ccDownload() { + client, query := NewRequest() + + uid, err := ofxgo.RandomUID() + if err != nil { + fmt.Println("Error creating uid for transaction:", err) + os.Exit(1) + } + + statementRequest := ofxgo.CCStatementRequest{ + TrnUID: *uid, + CCAcctFrom: ofxgo.CCAcct{ + AcctId: ofxgo.String(acctId), + }, + DtStart: ofxgo.Date(time.Unix(0, 0)), + DtEnd: ofxgo.Date(time.Now()), + Include: true, + } + query.CreditCards = append(query.CreditCards, &statementRequest) + + response, err := client.RequestNoParse(query) + + if err != nil { + fmt.Println("Error requesting account statement:", err) + os.Exit(1) + } + defer response.Body.Close() + + file, err := os.Create(filename) + if err != nil { + fmt.Println("Error creating file to write to:", err) + os.Exit(1) + } + defer file.Close() + + _, err = io.Copy(file, response.Body) + if err != nil { + fmt.Println("Error writing response to file:", err) + os.Exit(1) + } +} diff --git a/cmd/ofx/cctransactions.go b/cmd/ofx/cctransactions.go new file mode 100644 index 0000000..8133281 --- /dev/null +++ b/cmd/ofx/cctransactions.go @@ -0,0 +1,85 @@ +package main + +import ( + "flag" + "fmt" + "github.com/aclindsa/ofxgo" + "os" + "time" +) + +var ccTransactionsCommand = Command{ + Name: "transactions-cc", + Description: "Print credit card transactions and balance", + Flags: flag.NewFlagSet("transactions-cc", flag.ExitOnError), + CheckFlags: checkServerFlags, + Do: ccTransactions, +} + +func init() { + defineServerFlags(ccTransactionsCommand.Flags) + ccTransactionsCommand.Flags.StringVar(&acctId, "acctid", "", "AcctId (from `get-accounts` subcommand)") +} + +func ccTransactions() { + client, query := NewRequest() + + uid, err := ofxgo.RandomUID() + if err != nil { + fmt.Println("Error creating uid for transaction:", err) + os.Exit(1) + } + + statementRequest := ofxgo.CCStatementRequest{ + TrnUID: *uid, + CCAcctFrom: ofxgo.CCAcct{ + AcctId: ofxgo.String(acctId), + }, + DtStart: ofxgo.Date(time.Unix(0, 0)), + DtEnd: ofxgo.Date(time.Now()), + Include: true, + } + query.CreditCards = append(query.CreditCards, &statementRequest) + + response, err := client.Request(query) + if err != nil { + fmt.Println("Error requesting account statement:", err) + os.Exit(1) + } + + if response.Signon.Status.Code != 0 { + fmt.Printf("Nonzero signon status with message: %s\n", response.Signon.Status.Message) + os.Exit(1) + } + + if len(response.CreditCards) < 1 { + fmt.Println("No banking messages received") + return + } + + if stmt, ok := response.CreditCards[0].(ofxgo.CCStatementResponse); ok { + fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf) + fmt.Println("Transactions:") + for _, tran := range stmt.BankTranList.Transactions { + currency := stmt.CurDef + if len(tran.Currency) > 0 { + currency = tran.Currency + } else if len(tran.OrigCurrency) > 0 { + currency = tran.Currency + } + + var name string + if len(tran.Name) > 0 { + name = string(tran.Name) + } else { + name = string(tran.Payee.Name) + } + + if len(tran.Memo) > 0 { + name = name + " - " + string(tran.Memo) + } + + fmt.Printf("%s %-15s %-11s %s\n", tran.DtPosted, tran.TrnAmt.String()+" "+string(currency), tran.TrnType, name) + } + } +} diff --git a/cmd/ofx/command.go b/cmd/ofx/command.go new file mode 100644 index 0000000..a7e5f95 --- /dev/null +++ b/cmd/ofx/command.go @@ -0,0 +1,61 @@ +package main + +import ( + "flag" + "fmt" + "github.com/howeyc/gopass" +) + +type Command struct { + Name string + Description string + Flags *flag.FlagSet + CheckFlags func() bool // Check the flag values after they're parsed, printing errors and returning false if they're incorrect + Do func() // Run the command (only called if CheckFlags returns true) +} + +func (c *Command) Usage() { + fmt.Printf("Usage of %s:\n", c.Name) + c.Flags.PrintDefaults() +} + +// flags common to all server transactions +var serverURL, username, password, org, fid, appId, appVer, ofxVersion, clientUID string +var noIndentRequests bool + +func defineServerFlags(f *flag.FlagSet) { + f.StringVar(&serverURL, "url", "", "Financial institution's OFX Server URL (see ofxhome.com if you don't know it)") + f.StringVar(&username, "username", "", "Your username at financial institution") + f.StringVar(&password, "password", "", "Your password at financial institution") + f.StringVar(&org, "org", "", "'ORG' for your financial institution") + f.StringVar(&fid, "fid", "", "'FID' for your financial institution") + f.StringVar(&appId, "appid", "QWIN", "'APPID' to pretend to be") + f.StringVar(&appVer, "appver", "2400", "'APPVER' to pretend to be") + f.StringVar(&ofxVersion, "ofxversion", "203", "OFX version to use") + f.StringVar(&clientUID, "clientuid", "", "Client UID (only required by a few FIs, like Chase)") + f.BoolVar(&noIndentRequests, "noindent", false, "Don't indent OFX requests") +} + +func checkServerFlags() bool { + var ret bool = true + if len(serverURL) == 0 { + fmt.Println("Error: Server URL empty") + ret = false + } + if len(username) == 0 { + fmt.Println("Error: Username empty") + ret = false + } + + if ret && len(password) == 0 { + fmt.Printf("Password for %s: ", username) + pass, err := gopass.GetPasswd() + if err != nil { + fmt.Printf("Error reading password: %s\n", err) + ret = false + } else { + password = string(pass) + } + } + return ret +} diff --git a/cmd/ofx/get_accounts.go b/cmd/ofx/get_accounts.go new file mode 100644 index 0000000..fb27048 --- /dev/null +++ b/cmd/ofx/get_accounts.go @@ -0,0 +1,70 @@ +package main + +import ( + "flag" + "fmt" + "github.com/aclindsa/ofxgo" + "os" + "time" +) + +var getAccountsCommand = Command{ + Name: "get-accounts", + Description: "List accounts at your financial institution", + Flags: flag.NewFlagSet("get-accounts", flag.ExitOnError), + CheckFlags: checkServerFlags, + Do: getAccounts, +} + +func init() { + defineServerFlags(getAccountsCommand.Flags) +} + +func getAccounts() { + client, query := NewRequest() + + uid, err := ofxgo.RandomUID() + if err != nil { + fmt.Println("Error creating uid for transaction:", err) + os.Exit(1) + } + + acctInfo := ofxgo.AcctInfoRequest{ + TrnUID: *uid, + DtAcctUp: ofxgo.Date(time.Unix(0, 0)), + CltCookie: 1, + } + query.Signup = append(query.Signup, &acctInfo) + + signupResponse, err := client.Request(query) + if err != nil { + fmt.Println("Error requesting account information:", err) + os.Exit(1) + } + + if signupResponse.Signon.Status.Code != 0 { + fmt.Printf("Nonzero signon status with message: %s\n", signupResponse.Signon.Status.Message) + os.Exit(1) + } + + if len(signupResponse.Signup) < 1 { + fmt.Println("No signup messages received") + return + } + + fmt.Println("\nFound the following accounts:\n") + + if acctinfo, ok := signupResponse.Signup[0].(ofxgo.AcctInfoResponse); ok { + for _, acct := range acctinfo.AcctInfo { + if acct.BankAcctInfo != nil { + fmt.Printf("Bank Account:\n\tBankId: \"%s\"\n\tAcctId: \"%s\"\n\tAcctType: %s\n", acct.BankAcctInfo.BankAcctFrom.BankId, acct.BankAcctInfo.BankAcctFrom.AcctId, acct.BankAcctInfo.BankAcctFrom.AcctType) + } else if acct.CCAcctInfo != nil { + fmt.Printf("Credit card:\n\tAcctId: \"%s\"\n", acct.CCAcctInfo.CCAcctFrom.AcctId) + } else if acct.InvAcctInfo != nil { + fmt.Printf("Investment account:\n\tBrokerId: \"%s\"\n\tAcctId: \"%s\"\n", acct.InvAcctInfo.InvAcctFrom.BrokerId, acct.InvAcctInfo.InvAcctFrom.AcctId) + } else { + fmt.Printf("Unknown type: %s %s\n", acct.Name, acct.Desc) + } + } + } +} diff --git a/cmd/ofx/invdownload.go b/cmd/ofx/invdownload.go new file mode 100644 index 0000000..4e7f2d7 --- /dev/null +++ b/cmd/ofx/invdownload.go @@ -0,0 +1,87 @@ +package main + +import ( + "flag" + "fmt" + "github.com/aclindsa/ofxgo" + "io" + "os" + "time" +) + +var invDownloadCommand = Command{ + Name: "download-inv", + Description: "Download a investment account statement to a file", + Flags: flag.NewFlagSet("download-inv", flag.ExitOnError), + CheckFlags: invDownloadCheckFlags, + Do: invDownload, +} + +var brokerId string + +func init() { + defineServerFlags(invDownloadCommand.Flags) + invDownloadCommand.Flags.StringVar(&filename, "filename", "./download.ofx", "The file to save to") + invDownloadCommand.Flags.StringVar(&acctId, "acctid", "", "AcctId (from `get-accounts` subcommand)") + invDownloadCommand.Flags.StringVar(&brokerId, "brokerid", "", "BrokerId (from `get-accounts` subcommand)") +} + +func invDownloadCheckFlags() bool { + ret := checkServerFlags() + + if len(filename) == 0 { + fmt.Println("Error: Filename empty") + return false + } + + return ret +} + +func invDownload() { + client, query := NewRequest() + + uid, err := ofxgo.RandomUID() + if err != nil { + fmt.Println("Error creating uid for transaction:", err) + os.Exit(1) + } + + statementRequest := ofxgo.InvStatementRequest{ + TrnUID: *uid, + InvAcctFrom: ofxgo.InvAcct{ + BrokerId: ofxgo.String(brokerId), + AcctId: ofxgo.String(acctId), + }, + DtStart: ofxgo.Date(time.Unix(0, 0)), + DtEnd: ofxgo.Date(time.Now()), + Include: true, + IncludeOO: true, + IncludePos: true, + IncludeBalance: true, + Include401K: true, + Include401KBal: true, + } + query.Investments = append(query.Investments, &statementRequest) + + response, err := client.RequestNoParse(query) + + if err != nil { + fmt.Println("Error requesting account statement:", err) + + os.Exit(1) + } + defer response.Body.Close() + + file, err := os.Create(filename) + if err != nil { + fmt.Println("Error creating file to write to:", err) + os.Exit(1) + } + defer file.Close() + + _, err = io.Copy(file, response.Body) + if err != nil { + fmt.Println("Error writing response to file:", err) + os.Exit(1) + } +} diff --git a/cmd/ofx/invtransactions.go b/cmd/ofx/invtransactions.go new file mode 100644 index 0000000..f8860fa --- /dev/null +++ b/cmd/ofx/invtransactions.go @@ -0,0 +1,208 @@ +package main + +import ( + "flag" + "fmt" + "github.com/aclindsa/ofxgo" + "math/big" + "os" + "time" +) + +var invTransactionsCommand = Command{ + Name: "transactions-inv", + Description: "Print investment transactions", + Flags: flag.NewFlagSet("transactions-inv", flag.ExitOnError), + CheckFlags: checkServerFlags, + Do: invTransactions, +} + +func init() { + defineServerFlags(invTransactionsCommand.Flags) + invTransactionsCommand.Flags.StringVar(&acctId, "acctid", "", "AcctId (from `get-accounts` subcommand)") + invTransactionsCommand.Flags.StringVar(&brokerId, "brokerid", "", "BrokerId (from `get-accounts` subcommand)") +} + +func invTransactions() { + client, query := NewRequest() + + uid, err := ofxgo.RandomUID() + if err != nil { + fmt.Println("Error creating uid for transaction:", err) + os.Exit(1) + } + + statementRequest := ofxgo.InvStatementRequest{ + TrnUID: *uid, + InvAcctFrom: ofxgo.InvAcct{ + BrokerId: ofxgo.String(brokerId), + AcctId: ofxgo.String(acctId), + }, + DtStart: ofxgo.Date(time.Unix(0, 0)), + DtEnd: ofxgo.Date(time.Now()), + Include: true, + IncludeBalance: true, + // TODO Include401K: true, + // TODO Include401KBal: true, + } + query.Investments = append(query.Investments, &statementRequest) + + response, err := client.Request(query) + if err != nil { + fmt.Println("Error requesting account statement:", err) + + os.Exit(1) + } + + if response.Signon.Status.Code != 0 { + fmt.Printf("Nonzero signon status with message: %s\n", response.Signon.Status.Message) + os.Exit(1) + } + + if len(response.Investments) < 1 { + fmt.Println("No investment messages received") + return + } + + if stmt, ok := response.Investments[0].(ofxgo.InvStatementResponse); ok { + availCash := big.Rat(stmt.InvBal.AvailCash) + if availCash.IsInt() && availCash.Num().Int64() != 0 { + fmt.Printf("Balance: %s %s (as of %s)\n", stmt.InvBal.AvailCash, stmt.CurDef, stmt.DtAsOf) + } + for _, banktrans := range stmt.InvTranList.BankTransactions { + fmt.Printf("\nBank Transactions for %s subaccount:\n", banktrans.SubAcctFund) + for _, tran := range banktrans.Transactions { + printTransaction(stmt.CurDef, &tran) + } + } + fmt.Printf("\nInvestment Transactions:\n") + for _, t := range stmt.InvTranList.InvTransactions { + fmt.Printf("%-14s", t.TransactionType()) + switch tran := t.(type) { + case ofxgo.BuyDebt: + printInvBuy(stmt.CurDef, &tran.InvBuy) + case ofxgo.BuyMF: + printInvBuy(stmt.CurDef, &tran.InvBuy) + case ofxgo.BuyOpt: + printInvBuy(stmt.CurDef, &tran.InvBuy) + case ofxgo.BuyOther: + printInvBuy(stmt.CurDef, &tran.InvBuy) + case ofxgo.BuyStock: + printInvBuy(stmt.CurDef, &tran.InvBuy) + case ofxgo.ClosureOpt: + printInvTran(&tran.InvTran) + fmt.Println("%s %s contracts (%s shares each)\n", tran.OptAction, tran.Units, tran, tran.ShPerCtrct) + case ofxgo.Income: + printInvTran(&tran.InvTran) + currency := stmt.CurDef + if len(tran.Currency.CurSym) > 0 { + currency = tran.Currency.CurSym + } else if len(tran.OrigCurrency.CurSym) > 0 { + currency = tran.Currency.CurSym + } + fmt.Printf(" %s %s %s (%s %s)\n", tran.IncomeType, tran.Total, currency, tran.SecId.UniqueIdType, tran.SecId.UniqueId) + // TODO print ticker instead of CUSIP + case ofxgo.InvExpense: + printInvTran(&tran.InvTran) + currency := stmt.CurDef + if len(tran.Currency.CurSym) > 0 { + currency = tran.Currency.CurSym + } else if len(tran.OrigCurrency.CurSym) > 0 { + currency = tran.Currency.CurSym + } + fmt.Printf(" %s %s (%s %s)\n", tran.Total, currency, tran.SecId.UniqueIdType, tran.SecId.UniqueId) + // TODO print ticker instead of CUSIP + case ofxgo.JrnlFund: + printInvTran(&tran.InvTran) + fmt.Printf(" %s %s (%s -> %s)\n", tran.Total, stmt.CurDef, tran.SubAcctFrom, tran.SubAcctTo) + case ofxgo.JrnlSec: + printInvTran(&tran.InvTran) + fmt.Printf(" %s %s %s (%s -> %s)\n", tran.Units, tran.SecId.UniqueIdType, tran.SecId.UniqueId, tran.SubAcctFrom, tran.SubAcctTo) + // TODO print ticker instead of CUSIP + case ofxgo.MarginInterest: + printInvTran(&tran.InvTran) + currency := stmt.CurDef + if len(tran.Currency.CurSym) > 0 { + currency = tran.Currency.CurSym + } else if len(tran.OrigCurrency.CurSym) > 0 { + currency = tran.Currency.CurSym + } + fmt.Printf(" %s %s\n", tran.Total, currency) + case ofxgo.Reinvest: + printInvTran(&tran.InvTran) + currency := stmt.CurDef + if len(tran.Currency.CurSym) > 0 { + currency = tran.Currency.CurSym + } else if len(tran.OrigCurrency.CurSym) > 0 { + currency = tran.Currency.CurSym + } + fmt.Printf(" %s (%s %s)@%s %s (Total: %s)\n", tran.Units, tran.SecId.UniqueIdType, tran.SecId.UniqueId, tran.UnitPrice, currency, tran.Total) + // TODO print ticker instead of CUSIP + case ofxgo.RetOfCap: + printInvTran(&tran.InvTran) + currency := stmt.CurDef + if len(tran.Currency.CurSym) > 0 { + currency = tran.Currency.CurSym + } else if len(tran.OrigCurrency.CurSym) > 0 { + currency = tran.Currency.CurSym + } + fmt.Printf(" %s %s (%s %s)\n", tran.Total, currency, tran.SecId.UniqueIdType, tran.SecId.UniqueId) + // TODO print ticker instead of CUSIP + case ofxgo.SellDebt: + printInvSell(stmt.CurDef, &tran.InvSell) + case ofxgo.SellMF: + printInvSell(stmt.CurDef, &tran.InvSell) + case ofxgo.SellOpt: + printInvSell(stmt.CurDef, &tran.InvSell) + case ofxgo.SellOther: + printInvSell(stmt.CurDef, &tran.InvSell) + case ofxgo.SellStock: + printInvSell(stmt.CurDef, &tran.InvSell) + case ofxgo.Split: + printInvTran(&tran.InvTran) + currency := stmt.CurDef + if len(tran.Currency.CurSym) > 0 { + currency = tran.Currency.CurSym + } else if len(tran.OrigCurrency.CurSym) > 0 { + currency = tran.Currency.CurSym + } + fmt.Printf(" %s/%s %s -> %s shares of %s %s (%s %s for fractional shares)\n", tran.Numerator, tran.Denominator, tran.OldUnits, tran.NewUnits, tran.SecId.UniqueIdType, tran.SecId.UniqueId, tran.FracCash, currency) + // TODO print ticker instead of CUSIP + case ofxgo.Transfer: + printInvTran(&tran.InvTran) + fmt.Printf(" %s (%s %s) %s\n", tran.Units, tran.SecId.UniqueIdType, tran.SecId.UniqueId, tran.TferAction) + // TODO print ticker instead of CUSIP + } + } + } +} + +func printInvTran(it *ofxgo.InvTran) { + fmt.Printf("%s", it.DtTrade) +} + +func printInvBuy(defCurrency ofxgo.String, ib *ofxgo.InvBuy) { + printInvTran(&ib.InvTran) + currency := defCurrency + if len(ib.Currency.CurSym) > 0 { + currency = ib.Currency.CurSym + } else if len(ib.OrigCurrency.CurSym) > 0 { + currency = ib.Currency.CurSym + } + + fmt.Printf("%s (%s %s)@%s %s (Total: %s)\n", ib.Units, ib.SecId.UniqueIdType, ib.SecId.UniqueId, ib.UnitPrice, currency, ib.Total) + // TODO print ticker instead of CUSIP +} + +func printInvSell(defCurrency ofxgo.String, is *ofxgo.InvSell) { + printInvTran(&is.InvTran) + currency := defCurrency + if len(is.Currency.CurSym) > 0 { + currency = is.Currency.CurSym + } else if len(is.OrigCurrency.CurSym) > 0 { + currency = is.Currency.CurSym + } + + fmt.Printf(" %s (%s %s)@%s %s (Total: %s)\n", is.Units, is.SecId.UniqueIdType, is.SecId.UniqueId, is.UnitPrice, currency, is.Total) + // TODO print ticker instead of CUSIP +} diff --git a/cmd/ofx/main.go b/cmd/ofx/main.go new file mode 100644 index 0000000..cd755be --- /dev/null +++ b/cmd/ofx/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "fmt" + "os" + "strconv" +) + +var commands = []Command{ + getAccountsCommand, + downloadCommand, + ccDownloadCommand, + invDownloadCommand, + bankTransactionsCommand, + ccTransactionsCommand, + invTransactionsCommand, +} + +func usage() { + fmt.Println(`The ofxgo command-line client provides a simple interface to +query, parse, and display financial data via the OFX specification. + +Usage: + ofx command [arguments] + +The commands are:`) + + maxlen := 0 + for _, cmd := range commands { + if len(cmd.Name) > maxlen { + maxlen = len(cmd.Name) + } + } + formatString := " %-" + strconv.Itoa(maxlen) + "s %s\n" + + for _, cmd := range commands { + fmt.Printf(formatString, cmd.Name, cmd.Description) + } +} + +func runCmd(c *Command) { + err := c.Flags.Parse(os.Args[2:]) + if err != nil { + fmt.Printf("Error parsing flags: %s\n", err) + c.Usage() + os.Exit(1) + } + + if !c.CheckFlags() { + fmt.Println() + c.Usage() + os.Exit(1) + } + + c.Do() +} + +func main() { + if len(os.Args) < 2 { + fmt.Println("Error: Please supply a sub-command. Usage:\n") + usage() + os.Exit(1) + } + cmdName := os.Args[1] + for _, cmd := range commands { + if cmd.Name == cmdName { + runCmd(&cmd) + os.Exit(0) + } + } + + switch cmdName { + case "-h", "-help", "--help", "help": + usage() + default: + fmt.Println("Error: Invalid sub-command. Usage:") + usage() + os.Exit(1) + } +} diff --git a/cmd/ofx/util.go b/cmd/ofx/util.go new file mode 100644 index 0000000..817209d --- /dev/null +++ b/cmd/ofx/util.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/aclindsa/ofxgo" +) + +func NewRequest() (*ofxgo.Client, *ofxgo.Request) { + var client = ofxgo.Client{ + AppId: appId, + AppVer: appVer, + SpecVersion: ofxVersion, + NoIndent: noIndentRequests, + } + + var query ofxgo.Request + query.URL = serverURL + query.Signon.ClientUID = ofxgo.UID(clientUID) + query.Signon.UserId = ofxgo.String(username) + query.Signon.UserPass = ofxgo.String(password) + query.Signon.Org = ofxgo.String(org) + query.Signon.Fid = ofxgo.String(fid) + + return &client, &query +}