mirror of
https://github.com/aclindsa/ofxgo.git
synced 2025-07-03 20:38:39 -04:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
12ea3b7e8b | |||
e76c697cad | |||
2641443ebe | |||
01b26887af | |||
2b8a79e4b7 | |||
9136c9bab2 | |||
0d93a42626 | |||
56ca46714b | |||
4c7c48cab7 | |||
8c1e6eafab | |||
52f3e4120b | |||
ef87cc536c | |||
830a6064c7 | |||
6807c93e0e | |||
10edd94920 | |||
d88d45a664 | |||
2caa23564a | |||
5923a34de0 | |||
aa4d8074b2 | |||
65cc26a0db | |||
8f3e7309f2 | |||
631508ccc9 | |||
60a5707de6 | |||
3240ef383b |
34
.github/workflows/test.yml
vendored
Normal file
34
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: ofxgo CI Test
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.13.x, 1.15.x, 1.16.x]
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Test
|
||||||
|
run: go test -v -covermode=count -coverprofile="profile.cov" ./...
|
||||||
|
- name: Send Coverage
|
||||||
|
uses: shogo82148/actions-goveralls@v1
|
||||||
|
with:
|
||||||
|
path-to-profile: "profile.cov"
|
||||||
|
flag-name: ${{ matrix.os }}-go-${{ matrix.go-version }}
|
||||||
|
parallel: true
|
||||||
|
# notifies that all test jobs are finished.
|
||||||
|
finish:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: shogo82148/actions-goveralls@v1
|
||||||
|
with:
|
||||||
|
parallel-finished: true
|
20
.travis.yml
20
.travis.yml
@ -1,20 +0,0 @@
|
|||||||
language: go
|
|
||||||
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.9.x
|
|
||||||
- 1.12.x
|
|
||||||
- master
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
# Fetch/build coverage reporting tools
|
|
||||||
- go get github.com/mattn/goveralls
|
|
||||||
- go install github.com/mattn/goveralls
|
|
||||||
|
|
||||||
script:
|
|
||||||
- go test -v -covermode=count -coverprofile=coverage.out
|
|
||||||
|
|
||||||
after_script:
|
|
||||||
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN
|
|
31
README.md
31
README.md
@ -1,9 +1,9 @@
|
|||||||
# OFXGo
|
# OFXGo
|
||||||
|
|
||||||
[](https://goreportcard.com/report/github.com/aclindsa/ofxgo)
|
[](https://goreportcard.com/report/github.com/aclindsa/ofxgo)
|
||||||
[](https://travis-ci.org/aclindsa/ofxgo)
|
[](https://github.com/aclindsa/ofxgo/actions?query=workflow%3A%22ofxgo+CI+Test%22+branch%3Amaster)
|
||||||
[](https://coveralls.io/github/aclindsa/ofxgo?branch=master)
|
[](https://coveralls.io/github/aclindsa/ofxgo?branch=master)
|
||||||
[](https://godoc.org/github.com/aclindsa/ofxgo)
|
[](https://pkg.go.dev/github.com/aclindsa/ofxgo)
|
||||||
|
|
||||||
**OFXGo** is a library for querying OFX servers and/or parsing the responses. It
|
**OFXGo** is a library for querying OFX servers and/or parsing the responses. It
|
||||||
also provides an example command-line client to demonstrate the use of the
|
also provides an example command-line client to demonstrate the use of the
|
||||||
@ -13,7 +13,7 @@ library.
|
|||||||
|
|
||||||
The main purpose of this project is to provide a library to make it easier to
|
The main purpose of this project is to provide a library to make it easier to
|
||||||
query financial information with OFX from the comfort of Golang, without having
|
query financial information with OFX from the comfort of Golang, without having
|
||||||
to marshal/unmarshal to SGML or XML. The library does *not* intend to abstract
|
to marshal/unmarshal to SGML or XML. The library does _not_ intend to abstract
|
||||||
away all of the details of the OFX specification, which would be difficult to do
|
away all of the details of the OFX specification, which would be difficult to do
|
||||||
well. Instead, it exposes the OFX SGML/XML hierarchy as structs which mostly
|
well. Instead, it exposes the OFX SGML/XML hierarchy as structs which mostly
|
||||||
resemble it. Its primary goal is to enable the creation of other personal
|
resemble it. Its primary goal is to enable the creation of other personal
|
||||||
@ -36,7 +36,7 @@ repository.
|
|||||||
## Library documentation
|
## Library documentation
|
||||||
|
|
||||||
Documentation can be found with the `go doc` tool, or at
|
Documentation can be found with the `go doc` tool, or at
|
||||||
https://godoc.org/github.com/aclindsa/ofxgo
|
https://pkg.go.dev/github.com/aclindsa/ofxgo
|
||||||
|
|
||||||
## Example Usage
|
## Example Usage
|
||||||
|
|
||||||
@ -94,9 +94,30 @@ if stmt, ok := response.Bank[0].(*ofxgo.StatementResponse); ok {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Similarly, if you have an OFX file available locally, you can parse it directly:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
f, err := os.Open("./transactions.qfx")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("can't open file: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
resp, err := ofxgo.ParseResponse(f)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("can't parse response: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// do something with resp (*ofxgo.Response)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
OFXGo requires go >= 1.9
|
OFXGo requires go >= 1.12
|
||||||
|
|
||||||
## Using the command-line client
|
## Using the command-line client
|
||||||
|
|
||||||
|
@ -21,6 +21,8 @@ type BasicClient struct {
|
|||||||
NoIndent bool
|
NoIndent bool
|
||||||
// Use carriage returns on new lines
|
// Use carriage returns on new lines
|
||||||
CarriageReturn bool
|
CarriageReturn bool
|
||||||
|
// Set User-Agent header to this string, if not empty
|
||||||
|
UserAgent string
|
||||||
}
|
}
|
||||||
|
|
||||||
// OfxVersion returns the OFX specification version this BasicClient will marshal
|
// OfxVersion returns the OFX specification version this BasicClient will marshal
|
||||||
@ -61,27 +63,41 @@ func (c *BasicClient) CarriageReturnNewLines() bool {
|
|||||||
return c.CarriageReturn
|
return c.CarriageReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RawRequest is a convenience wrapper around http.Post. It is exposed only for
|
||||||
|
// when you need to read/inspect the raw HTTP response yourself.
|
||||||
func (c *BasicClient) RawRequest(URL string, r io.Reader) (*http.Response, error) {
|
func (c *BasicClient) RawRequest(URL string, r io.Reader) (*http.Response, error) {
|
||||||
if !strings.HasPrefix(URL, "https://") {
|
if !strings.HasPrefix(URL, "https://") {
|
||||||
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := http.Post(URL, "application/x-ofx", r)
|
request, err := http.NewRequest("POST", URL, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
request.Header.Set("Content-Type", "application/x-ofx")
|
||||||
|
if c.UserAgent != "" {
|
||||||
|
request.Header.Set("User-Agent", c.UserAgent)
|
||||||
|
}
|
||||||
|
response, err := http.DefaultClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.StatusCode != 200 {
|
if response.StatusCode != 200 {
|
||||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
return response, errors.New("OFXQuery request status: " + response.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestNoParse marshals a Request to XML, makes an HTTP request, and returns
|
||||||
|
// the raw HTTP response
|
||||||
func (c *BasicClient) RequestNoParse(r *Request) (*http.Response, error) {
|
func (c *BasicClient) RequestNoParse(r *Request) (*http.Response, error) {
|
||||||
return clientRequestNoParse(c, r)
|
return clientRequestNoParse(c, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request marshals a Request to XML, makes an HTTP request, and then
|
||||||
|
// unmarshals the response into a Response object.
|
||||||
func (c *BasicClient) Request(r *Request) (*Response, error) {
|
func (c *BasicClient) Request(r *Request) (*Response, error) {
|
||||||
return clientRequest(c, r)
|
return clientRequest(c, r)
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ var filename, bankID, acctID, acctType string
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
defineServerFlags(downloadCommand.Flags)
|
defineServerFlags(downloadCommand.Flags)
|
||||||
downloadCommand.Flags.StringVar(&filename, "filename", "./download.ofx", "The file to save to")
|
downloadCommand.Flags.StringVar(&filename, "filename", "./response.ofx", "The file to save to")
|
||||||
downloadCommand.Flags.StringVar(&bankID, "bankid", "", "BankID (from `get-accounts` subcommand)")
|
downloadCommand.Flags.StringVar(&bankID, "bankid", "", "BankID (from `get-accounts` subcommand)")
|
||||||
downloadCommand.Flags.StringVar(&acctID, "acctid", "", "AcctID (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)")
|
downloadCommand.Flags.StringVar(&acctType, "accttype", "CHECKING", "AcctType (from `get-accounts` subcommand)")
|
||||||
|
@ -18,7 +18,7 @@ var ccDownloadCommand = command{
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
defineServerFlags(ccDownloadCommand.Flags)
|
defineServerFlags(ccDownloadCommand.Flags)
|
||||||
ccDownloadCommand.Flags.StringVar(&filename, "filename", "./download.ofx", "The file to save to")
|
ccDownloadCommand.Flags.StringVar(&filename, "filename", "./response.ofx", "The file to save to")
|
||||||
ccDownloadCommand.Flags.StringVar(&acctID, "acctid", "", "AcctID (from `get-accounts` subcommand)")
|
ccDownloadCommand.Flags.StringVar(&acctID, "acctid", "", "AcctID (from `get-accounts` subcommand)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,9 @@ func (c *command) usage() {
|
|||||||
// flags common to all server transactions
|
// flags common to all server transactions
|
||||||
var serverURL, username, password, org, fid, appID, appVer, ofxVersion, clientUID string
|
var serverURL, username, password, org, fid, appID, appVer, ofxVersion, clientUID string
|
||||||
var noIndentRequests bool
|
var noIndentRequests bool
|
||||||
|
var carriageReturn bool
|
||||||
|
var dryrun bool
|
||||||
|
var userAgent string
|
||||||
|
|
||||||
func defineServerFlags(f *flag.FlagSet) {
|
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(&serverURL, "url", "", "Financial institution's OFX Server URL (see ofxhome.com if you don't know it)")
|
||||||
@ -34,6 +37,9 @@ func defineServerFlags(f *flag.FlagSet) {
|
|||||||
f.StringVar(&ofxVersion, "ofxversion", "203", "OFX version to use")
|
f.StringVar(&ofxVersion, "ofxversion", "203", "OFX version to use")
|
||||||
f.StringVar(&clientUID, "clientuid", "", "Client UID (only required by a few FIs, like Chase)")
|
f.StringVar(&clientUID, "clientuid", "", "Client UID (only required by a few FIs, like Chase)")
|
||||||
f.BoolVar(&noIndentRequests, "noindent", false, "Don't indent OFX requests")
|
f.BoolVar(&noIndentRequests, "noindent", false, "Don't indent OFX requests")
|
||||||
|
f.BoolVar(&carriageReturn, "carriagereturn", false, "Use carriage return as line separator")
|
||||||
|
f.StringVar(&userAgent, "useragent", "", "Use string as User-Agent header when sending request")
|
||||||
|
f.BoolVar(&dryrun, "dryrun", false, "Don't send request - print content of request instead")
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkServerFlags() bool {
|
func checkServerFlags() bool {
|
||||||
|
@ -20,7 +20,7 @@ var brokerID string
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
defineServerFlags(invDownloadCommand.Flags)
|
defineServerFlags(invDownloadCommand.Flags)
|
||||||
invDownloadCommand.Flags.StringVar(&filename, "filename", "./download.ofx", "The file to save to")
|
invDownloadCommand.Flags.StringVar(&filename, "filename", "./response.ofx", "The file to save to")
|
||||||
invDownloadCommand.Flags.StringVar(&acctID, "acctid", "", "AcctID (from `get-accounts` subcommand)")
|
invDownloadCommand.Flags.StringVar(&acctID, "acctid", "", "AcctID (from `get-accounts` subcommand)")
|
||||||
invDownloadCommand.Flags.StringVar(&brokerID, "brokerid", "", "BrokerID (from `get-accounts` subcommand)")
|
invDownloadCommand.Flags.StringVar(&brokerID, "brokerid", "", "BrokerID (from `get-accounts` subcommand)")
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var commands = []command{
|
var commands = []command{
|
||||||
|
profileDownloadCommand,
|
||||||
getAccountsCommand,
|
getAccountsCommand,
|
||||||
downloadCommand,
|
downloadCommand,
|
||||||
ccDownloadCommand,
|
ccDownloadCommand,
|
||||||
|
81
cmd/ofx/profiledownload.go
Normal file
81
cmd/ofx/profiledownload.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/aclindsa/ofxgo"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var profileDownloadCommand = command{
|
||||||
|
Name: "download-profile",
|
||||||
|
Description: "Download a FI profile to a file",
|
||||||
|
Flags: flag.NewFlagSet("download-profile", flag.ExitOnError),
|
||||||
|
CheckFlags: downloadProfileCheckFlags,
|
||||||
|
Do: downloadProfile,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
defineServerFlags(profileDownloadCommand.Flags)
|
||||||
|
profileDownloadCommand.Flags.StringVar(&filename, "filename", "./response.ofx", "The file to save to")
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadProfileCheckFlags() bool {
|
||||||
|
// Assume if the user didn't specify username that we should use anonymous
|
||||||
|
// values for it and password
|
||||||
|
if len(username) == 0 {
|
||||||
|
username = "anonymous00000000000000000000000"
|
||||||
|
password = "anonymous00000000000000000000000"
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := checkServerFlags()
|
||||||
|
|
||||||
|
if len(filename) == 0 {
|
||||||
|
fmt.Println("Error: Filename empty")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadProfile() {
|
||||||
|
client, query := newRequest()
|
||||||
|
|
||||||
|
uid, err := ofxgo.RandomUID()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error creating uid for transaction:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
profileRequest := ofxgo.ProfileRequest{
|
||||||
|
TrnUID: *uid,
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Prof = append(query.Prof, &profileRequest)
|
||||||
|
|
||||||
|
if dryrun {
|
||||||
|
printRequest(client, query)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.RequestNoParse(query)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error requesting FI profile:", 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)
|
||||||
|
}
|
||||||
|
}
|
@ -14,10 +14,12 @@ func newRequest() (ofxgo.Client, *ofxgo.Request) {
|
|||||||
}
|
}
|
||||||
var client = ofxgo.GetClient(serverURL,
|
var client = ofxgo.GetClient(serverURL,
|
||||||
&ofxgo.BasicClient{
|
&ofxgo.BasicClient{
|
||||||
AppID: appID,
|
AppID: appID,
|
||||||
AppVer: appVer,
|
AppVer: appVer,
|
||||||
SpecVersion: ver,
|
SpecVersion: ver,
|
||||||
NoIndent: noIndentRequests,
|
NoIndent: noIndentRequests,
|
||||||
|
CarriageReturn: carriageReturn,
|
||||||
|
UserAgent: userAgent,
|
||||||
})
|
})
|
||||||
|
|
||||||
var query ofxgo.Request
|
var query ofxgo.Request
|
||||||
@ -30,3 +32,14 @@ func newRequest() (ofxgo.Client, *ofxgo.Request) {
|
|||||||
|
|
||||||
return client, &query
|
return client, &query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printRequest(c ofxgo.Client, r *ofxgo.Request) {
|
||||||
|
r.SetClientFields(c)
|
||||||
|
|
||||||
|
b, err := r.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(b)
|
||||||
|
}
|
||||||
|
@ -20,7 +20,7 @@ type DiscoverCardClient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewDiscoverCardClient returns a Client interface configured to handle
|
// NewDiscoverCardClient returns a Client interface configured to handle
|
||||||
// Discover Card's brand of idiosyncracy
|
// Discover Card's brand of idiosyncrasy
|
||||||
func NewDiscoverCardClient(bc *BasicClient) Client {
|
func NewDiscoverCardClient(bc *BasicClient) Client {
|
||||||
return &DiscoverCardClient{bc}
|
return &DiscoverCardClient{bc}
|
||||||
}
|
}
|
||||||
@ -77,6 +77,8 @@ func discoverCardHTTPPost(URL string, r io.Reader) (*http.Response, error) {
|
|||||||
return http.ReadResponse(bufio.NewReader(conn), nil)
|
return http.ReadResponse(bufio.NewReader(conn), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RawRequest is a convenience wrapper around http.Post. It is exposed only for
|
||||||
|
// when you need to read/inspect the raw HTTP response yourself.
|
||||||
func (c *DiscoverCardClient) RawRequest(URL string, r io.Reader) (*http.Response, error) {
|
func (c *DiscoverCardClient) RawRequest(URL string, r io.Reader) (*http.Response, error) {
|
||||||
if !strings.HasPrefix(URL, "https://") {
|
if !strings.HasPrefix(URL, "https://") {
|
||||||
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||||
@ -94,10 +96,14 @@ func (c *DiscoverCardClient) RawRequest(URL string, r io.Reader) (*http.Response
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestNoParse marshals a Request to XML, makes an HTTP request, and returns
|
||||||
|
// the raw HTTP response
|
||||||
func (c *DiscoverCardClient) RequestNoParse(r *Request) (*http.Response, error) {
|
func (c *DiscoverCardClient) RequestNoParse(r *Request) (*http.Response, error) {
|
||||||
return clientRequestNoParse(c, r)
|
return clientRequestNoParse(c, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request marshals a Request to XML, makes an HTTP request, and then
|
||||||
|
// unmarshals the response into a Response object.
|
||||||
func (c *DiscoverCardClient) Request(r *Request) (*Response, error) {
|
func (c *DiscoverCardClient) Request(r *Request) (*Response, error) {
|
||||||
return clientRequest(c, r)
|
return clientRequest(c, r)
|
||||||
}
|
}
|
||||||
|
8
go.mod
8
go.mod
@ -2,10 +2,10 @@ module github.com/aclindsa/ofxgo
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aclindsa/xml v0.0.0-20190701095008-453d2c6090c2
|
github.com/aclindsa/xml v0.0.0-20190701095008-453d2c6090c2
|
||||||
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
|
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c
|
||||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 // indirect
|
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 // indirect
|
golang.org/x/sys v0.0.0-20201006155630-ac719f4daadf // indirect
|
||||||
golang.org/x/text v0.0.0-20180911161511-905a57155faa
|
golang.org/x/text v0.3.3
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.9
|
go 1.9
|
||||||
|
26
go.sum
26
go.sum
@ -1,14 +1,16 @@
|
|||||||
github.com/aclindsa/xml v0.0.0-20171002130543-5d4402bb4a20 h1:wN3KlzWq56AIgOqFzYLYVih4zVyPDViCUeG5uZxJHq4=
|
|
||||||
github.com/aclindsa/xml v0.0.0-20171002130543-5d4402bb4a20/go.mod h1:DiEHtTD+e6zS3+R95F05Bfbcsfv13wZTi2M4LfAFLBE=
|
|
||||||
github.com/aclindsa/xml v0.0.0-20190625094425-0aa7a3409cf4 h1:STo5wlCItpgL9LFBui17kZ/N1iKQk+UztLRj2cVkSXQ=
|
|
||||||
github.com/aclindsa/xml v0.0.0-20190625094425-0aa7a3409cf4/go.mod h1:GjqOUT8xlg5+T19lFv6yAGNrtMKkZ839Gt4e16mBXlY=
|
|
||||||
github.com/aclindsa/xml v0.0.0-20190701095008-453d2c6090c2 h1:ICeGSGrc6fd81VtQ3nZ2h7GEOKxWYwRxjW0v0d/mgu4=
|
github.com/aclindsa/xml v0.0.0-20190701095008-453d2c6090c2 h1:ICeGSGrc6fd81VtQ3nZ2h7GEOKxWYwRxjW0v0d/mgu4=
|
||||||
github.com/aclindsa/xml v0.0.0-20190701095008-453d2c6090c2/go.mod h1:GjqOUT8xlg5+T19lFv6yAGNrtMKkZ839Gt4e16mBXlY=
|
github.com/aclindsa/xml v0.0.0-20190701095008-453d2c6090c2/go.mod h1:GjqOUT8xlg5+T19lFv6yAGNrtMKkZ839Gt4e16mBXlY=
|
||||||
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
|
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c h1:aY2hhxLhjEAbfXOx2nRJxCXezC6CO2V/yN+OCr1srtk=
|
||||||
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
|
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
|
||||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 h1:Vk3wNqEZwyGyei9yq5ekj7frek2u7HUfffJ1/opblzc=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
|
||||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611 h1:O33LKL7WyJgjN9CvxfTIomjIClbd/Kq86/iipowHQU0=
|
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/sys v0.0.0-20180928133829-e4b3c5e90611/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/text v0.0.0-20180911161511-905a57155faa h1:uIJ7KxPgS7ODNO//HqlPfjWmWDGRsoONAVcEVaJNWNs=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/text v0.0.0-20180911161511-905a57155faa/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201006155630-ac719f4daadf h1:Bg47KQy0JhTHuf4sLiQwTMKwUMfSDwgSGatrxGR7nLM=
|
||||||
|
golang.org/x/sys v0.0.0-20201006155630-ac719f4daadf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
10
invstmt.go
10
invstmt.go
@ -103,7 +103,7 @@ type InvSell struct {
|
|||||||
Taxes Amount `xml:"TAXES,omitempty"`
|
Taxes Amount `xml:"TAXES,omitempty"`
|
||||||
Fees Amount `xml:"FEES,omitempty"`
|
Fees Amount `xml:"FEES,omitempty"`
|
||||||
Load Amount `xml:"LOAD,omitempty"`
|
Load Amount `xml:"LOAD,omitempty"`
|
||||||
Witholding Amount `xml:"WITHHOLDING,omitempty"` // Federal tax witholdings
|
Withholding Amount `xml:"WITHHOLDING,omitempty"` // Federal tax withholdings
|
||||||
TaxExempt Boolean `xml:"TAXEXEMPT,omitempty"` // Tax-exempt transaction
|
TaxExempt Boolean `xml:"TAXEXEMPT,omitempty"` // Tax-exempt transaction
|
||||||
Total Amount `xml:"TOTAL"` // Transaction total. Buys, sells, etc.:((quan. * (price +/- markup/markdown)) +/-(commission + fees + load + taxes + penalty + withholding + statewithholding)). Distributions, interest, margin interest, misc. expense, etc.: amount. Return of cap: cost basis
|
Total Amount `xml:"TOTAL"` // Transaction total. Buys, sells, etc.:((quan. * (price +/- markup/markdown)) +/-(commission + fees + load + taxes + penalty + withholding + statewithholding)). Distributions, interest, margin interest, misc. expense, etc.: amount. Return of cap: cost basis
|
||||||
Gain Amount `xml:"GAIN,omitempty"` // Total gain
|
Gain Amount `xml:"GAIN,omitempty"` // Total gain
|
||||||
@ -112,9 +112,9 @@ type InvSell struct {
|
|||||||
SubAcctSec subAcctType `xml:"SUBACCTSEC"` // Sub-account type for this security. One of CASH, MARGIN, SHORT, OTHER
|
SubAcctSec subAcctType `xml:"SUBACCTSEC"` // Sub-account type for this security. One of CASH, MARGIN, SHORT, OTHER
|
||||||
SubAcctFund subAcctType `xml:"SUBACCTFUND"` // Where did the money for the transaction come from or go to? CASH, MARGIN, SHORT, OTHER
|
SubAcctFund subAcctType `xml:"SUBACCTFUND"` // Where did the money for the transaction come from or go to? CASH, MARGIN, SHORT, OTHER
|
||||||
|
|
||||||
LoanID String `xml:"LOANID,omitempty"` // For 401(k) accounts only. Indicates that the transaction was due to a loan or a loan repayment, and which loan it was
|
LoanID String `xml:"LOANID,omitempty"` // For 401(k) accounts only. Indicates that the transaction was due to a loan or a loan repayment, and which loan it was
|
||||||
StateWitholding Amount `xml:"STATEWITHHOLDING,omitempty"` // State tax witholdings
|
StateWithholding Amount `xml:"STATEWITHHOLDING,omitempty"` // State tax withholdings
|
||||||
Penalty Amount `xml:"PENALTY,omitempty"` // Amount withheld due to penalty
|
Penalty Amount `xml:"PENALTY,omitempty"` // Amount withheld due to penalty
|
||||||
|
|
||||||
Inv401kSource inv401kSource `xml:"INV401KSOURCE,omitempty"` // Source of money for this transaction. One of PRETAX, AFTERTAX, MATCH, PROFITSHARING, ROLLOVER, OTHERVEST, OTHERNONVEST for 401(k) accounts. Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST
|
Inv401kSource inv401kSource `xml:"INV401KSOURCE,omitempty"` // Source of money for this transaction. One of PRETAX, AFTERTAX, MATCH, PROFITSHARING, ROLLOVER, OTHERVEST, OTHERNONVEST for 401(k) accounts. Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST
|
||||||
}
|
}
|
||||||
@ -210,7 +210,7 @@ type Income struct {
|
|||||||
SubAcctSec subAcctType `xml:"SUBACCTSEC"` // Sub-account type for this security. One of CASH, MARGIN, SHORT, OTHER
|
SubAcctSec subAcctType `xml:"SUBACCTSEC"` // Sub-account type for this security. One of CASH, MARGIN, SHORT, OTHER
|
||||||
SubAcctFund subAcctType `xml:"SUBACCTFUND"` // Where did the money for the transaction come from or go to? CASH, MARGIN, SHORT, OTHER
|
SubAcctFund subAcctType `xml:"SUBACCTFUND"` // Where did the money for the transaction come from or go to? CASH, MARGIN, SHORT, OTHER
|
||||||
TaxExempt Boolean `xml:"TAXEXEMPT,omitempty"` // Tax-exempt transaction
|
TaxExempt Boolean `xml:"TAXEXEMPT,omitempty"` // Tax-exempt transaction
|
||||||
Witholding Amount `xml:"WITHHOLDING,omitempty"` // Federal tax witholdings
|
Withholding Amount `xml:"WITHHOLDING,omitempty"` // Federal tax withholdings
|
||||||
Currency Currency `xml:"CURRENCY,omitempty"` // Represents the currency this transaction is in (instead of CURDEF in INVSTMTRS) if Valid()
|
Currency Currency `xml:"CURRENCY,omitempty"` // Represents the currency this transaction is in (instead of CURDEF in INVSTMTRS) if Valid()
|
||||||
OrigCurrency Currency `xml:"ORIGCURRENCY,omitempty"` // Represents the currency this transaction was converted to INVSTMTRS' CURDEF from if Valid
|
OrigCurrency Currency `xml:"ORIGCURRENCY,omitempty"` // Represents the currency this transaction was converted to INVSTMTRS' CURDEF from if Valid
|
||||||
Inv401kSource inv401kSource `xml:"INV401KSOURCE,omitempty"` // Source of money for this transaction. One of PRETAX, AFTERTAX, MATCH, PROFITSHARING, ROLLOVER, OTHERVEST, OTHERNONVEST for 401(k) accounts. Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST
|
Inv401kSource inv401kSource `xml:"INV401KSOURCE,omitempty"` // Source of money for this transaction. One of PRETAX, AFTERTAX, MATCH, PROFITSHARING, ROLLOVER, OTHERVEST, OTHERNONVEST for 401(k) accounts. Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST
|
||||||
|
94
response.go
94
response.go
@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aclindsa/xml"
|
"github.com/aclindsa/xml"
|
||||||
@ -35,78 +36,75 @@ type Response struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (or *Response) readSGMLHeaders(r *bufio.Reader) error {
|
func (or *Response) readSGMLHeaders(r *bufio.Reader) error {
|
||||||
var seenHeader, seenVersion bool = false, false
|
b, err := r.ReadSlice('<')
|
||||||
for {
|
if err != nil {
|
||||||
// Some financial institutions do not properly leave an empty line after the last header.
|
return err
|
||||||
// Avoid attempting to read another header in that case.
|
}
|
||||||
next, err := r.Peek(1)
|
|
||||||
if err != nil {
|
s := string(b)
|
||||||
return err
|
err = r.UnreadByte()
|
||||||
}
|
if err != nil {
|
||||||
if next[0] == '<' {
|
return err
|
||||||
break
|
}
|
||||||
|
|
||||||
|
// According to the latest OFX SGML spec (1.6), headers should be CRLF-separated
|
||||||
|
// and written as KEY:VALUE. However, some banks include a whitespace after the
|
||||||
|
// colon (KEY: VALUE), while others include no line breaks at all. The spec doesn't
|
||||||
|
// require a line break after the OFX headers, but it is allowed, and will be
|
||||||
|
// optionally captured & discarded by the trailing `\s*`. Valid SGML headers must
|
||||||
|
// always be present in exactly this order, so a regular expression is acceptable.
|
||||||
|
headerExp := regexp.MustCompile(
|
||||||
|
`^OFXHEADER:\s*(?P<OFXHEADER>\d+)\s*` +
|
||||||
|
`DATA:\s*(?P<DATA>[A-Z]+)\s*` +
|
||||||
|
`VERSION:\s*(?P<VERSION>\d+)\s*` +
|
||||||
|
`SECURITY:\s*(?P<SECURITY>[\w]+)\s*` +
|
||||||
|
`ENCODING:\s*(?P<ENCODING>[A-Z0-9-]+)\s*` +
|
||||||
|
`CHARSET:\s*(?P<CHARSET>[\w-]+)\s*` +
|
||||||
|
`COMPRESSION:\s*(?P<COMPRESSION>[A-Z]+)\s*` +
|
||||||
|
`OLDFILEUID:\s*(?P<OLDFILEUID>[\w-]+)\s*` +
|
||||||
|
`NEWFILEUID:\s*(?P<NEWFILEUID>[\w-]+)\s*<$`)
|
||||||
|
|
||||||
|
matches := headerExp.FindStringSubmatch(s)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return errors.New("OFX headers malformed")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, name := range headerExp.SubexpNames() {
|
||||||
|
if i == 0 {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
line, err := r.ReadString('\n')
|
headerValue := matches[i]
|
||||||
if err != nil {
|
switch name {
|
||||||
return err
|
|
||||||
}
|
|
||||||
// r.ReadString leaves the '\n' on the end...
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
|
|
||||||
if len(line) == 0 {
|
|
||||||
if seenHeader {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
header := strings.SplitN(line, ":", 2)
|
|
||||||
if header == nil || len(header) != 2 {
|
|
||||||
return errors.New("OFX headers malformed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some OFX servers put a space after the colon
|
|
||||||
headervalue := strings.TrimSpace(header[1])
|
|
||||||
|
|
||||||
switch header[0] {
|
|
||||||
case "OFXHEADER":
|
case "OFXHEADER":
|
||||||
if headervalue != "100" {
|
if headerValue != "100" {
|
||||||
return errors.New("OFXHEADER is not 100")
|
return errors.New("OFXHEADER is not 100")
|
||||||
}
|
}
|
||||||
seenHeader = true
|
|
||||||
case "DATA":
|
case "DATA":
|
||||||
if headervalue != "OFXSGML" {
|
if headerValue != "OFXSGML" {
|
||||||
return errors.New("OFX DATA header does not contain OFXSGML")
|
return errors.New("OFX DATA header does not contain OFXSGML")
|
||||||
}
|
}
|
||||||
case "VERSION":
|
case "VERSION":
|
||||||
err := or.Version.FromString(headervalue)
|
err := or.Version.FromString(headerValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
seenVersion = true
|
|
||||||
|
|
||||||
if or.Version > OfxVersion160 {
|
if or.Version > OfxVersion160 {
|
||||||
return errors.New("OFX VERSION > 160 in SGML header")
|
return errors.New("OFX VERSION > 160 in SGML header")
|
||||||
}
|
}
|
||||||
case "SECURITY":
|
case "SECURITY":
|
||||||
if headervalue != "NONE" {
|
if !(headerValue == "NONE" || headerValue == "TYPE1") {
|
||||||
return errors.New("OFX SECURITY header not NONE")
|
return errors.New("OFX SECURITY header must be NONE or TYPE1")
|
||||||
}
|
}
|
||||||
case "COMPRESSION":
|
case "COMPRESSION":
|
||||||
if headervalue != "NONE" {
|
if headerValue != "NONE" {
|
||||||
return errors.New("OFX COMPRESSION header not NONE")
|
return errors.New("OFX COMPRESSION header not NONE")
|
||||||
}
|
}
|
||||||
case "ENCODING", "CHARSET", "OLDFILEUID", "NEWFILEUID":
|
case "ENCODING", "CHARSET", "OLDFILEUID", "NEWFILEUID":
|
||||||
// TODO check/handle these headers?
|
// TODO: check/handle these headers?
|
||||||
default:
|
|
||||||
return errors.New("Invalid OFX header: " + header[0])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !seenVersion {
|
|
||||||
return errors.New("OFX VERSION header missing")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,8 +176,7 @@ func TestValidSamples(t *testing.T) {
|
|||||||
|
|
||||||
func TestInvalidResponse(t *testing.T) {
|
func TestInvalidResponse(t *testing.T) {
|
||||||
// in this example, the severity is invalid due to mixed upper and lower case letters
|
// in this example, the severity is invalid due to mixed upper and lower case letters
|
||||||
const invalidResponse = `
|
const invalidResponse = `OFXHEADER:100
|
||||||
OFXHEADER:100
|
|
||||||
DATA:OFXSGML
|
DATA:OFXSGML
|
||||||
VERSION:102
|
VERSION:102
|
||||||
SECURITY:NONE
|
SECURITY:NONE
|
||||||
|
1
samples/busted_responses/wellsfargo.qfx
Normal file
1
samples/busted_responses/wellsfargo.qfx
Normal file
@ -0,0 +1 @@
|
|||||||
|
OFXHEADER:100DATA:OFXSGMLVERSION:102SECURITY:NONEENCODING:USASCIICHARSET:1252COMPRESSION:NONEOLDFILEUID:NONENEWFILEUID:NONE<OFX><SIGNONMSGSRSV1><SONRS><STATUS><CODE>0<SEVERITY>INFO<MESSAGE>SUCCESS</STATUS><DTSERVER>20210102211014.201[-8:PST]<LANGUAGE>ENG<FI><ORG>WF<FID>1000</FI><SESSCOOKIE>abc-123<INTU.BID>1000<INTU.USERID>jane_doe</SONRS></SIGNONMSGSRSV1><BANKMSGSRSV1><STMTTRNRS><TRNUID>0<STATUS><CODE>0<SEVERITY>INFO<MESSAGE>SUCCESS</STATUS><STMTRS><CURDEF>USD<BANKACCTFROM><BANKID>123456789<ACCTID>9876543210<ACCTTYPE>CHECKING</BANKACCTFROM><BANKTRANLIST><DTSTART>20201201120000.000[-8:PST]<DTEND>20201231120000.000[-8:PST]<STMTTRN><TRNTYPE>DIRECTDEBIT<DTPOSTED>20201201120000.000[-8:PST]<TRNAMT>-12.34<FITID>202012011<NAME>AE Visa Card AE EPAY<MEMO> XXXXX1234</STMTTRN></BANKTRANLIST><LEDGERBAL><BALAMT>123.45<DTASOF>20201231120000.000[-8:PST]</LEDGERBAL><AVAILBAL><BALAMT>123.45<DTASOF>20201231120000.000[-8:PST]</AVAILBAL></STMTRS></STMTTRNRS></BANKMSGSRSV1></OFX>
|
75
samples/valid_responses/moneymrkt1_v103_TYPE1.ofx
Normal file
75
samples/valid_responses/moneymrkt1_v103_TYPE1.ofx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
OFXHEADER:100
|
||||||
|
DATA:OFXSGML
|
||||||
|
VERSION:103
|
||||||
|
SECURITY:TYPE1
|
||||||
|
ENCODING:USASCII
|
||||||
|
CHARSET:1252
|
||||||
|
COMPRESSION:NONE
|
||||||
|
OLDFILEUID:NONE
|
||||||
|
NEWFILEUID:NONE
|
||||||
|
|
||||||
|
<OFX>
|
||||||
|
<SIGNONMSGSRSV1><SONRS>
|
||||||
|
<STATUS>
|
||||||
|
<CODE>0
|
||||||
|
<SEVERITY>INFO
|
||||||
|
</STATUS>
|
||||||
|
<DTSERVER>20170407001840.607[0:GMT]
|
||||||
|
<LANGUAGE>ENG
|
||||||
|
<FI>
|
||||||
|
<ORG>UJKDO
|
||||||
|
<FID>3534
|
||||||
|
</FI>
|
||||||
|
</SONRS>
|
||||||
|
</SIGNONMSGSRSV1>
|
||||||
|
<BANKMSGSRSV1>
|
||||||
|
<STMTTRNRS>
|
||||||
|
<TRNUID>e1707dfd-695d-4451-8d9c-0e142fdc456a
|
||||||
|
<STATUS>
|
||||||
|
<CODE>0
|
||||||
|
<SEVERITY>INFO
|
||||||
|
</STATUS>
|
||||||
|
<STMTRS>
|
||||||
|
<CURDEF>USD
|
||||||
|
<BANKACCTFROM>
|
||||||
|
<BANKID>598813374
|
||||||
|
<ACCTID>35342483513
|
||||||
|
<ACCTTYPE>MONEYMRKT
|
||||||
|
</BANKACCTFROM>
|
||||||
|
<BANKTRANLIST>
|
||||||
|
<DTSTART>20170107011841.262[0:GMT]
|
||||||
|
<DTEND>20170407001841.262[0:GMT]
|
||||||
|
<STMTTRN>
|
||||||
|
<TRNTYPE>CREDIT
|
||||||
|
<DTPOSTED>20170117120000.000[0:GMT]
|
||||||
|
<TRNAMT>-995.4190396554627
|
||||||
|
<FITID>2fb2640c-cee3-4643-8ba3-ea21a4d18954
|
||||||
|
<NAME>Dividend Earned
|
||||||
|
</STMTTRN>
|
||||||
|
<STMTTRN>
|
||||||
|
<TRNTYPE>CREDIT
|
||||||
|
<DTPOSTED>20170215120000.000[0:GMT]
|
||||||
|
<TRNAMT>788.5385340523635
|
||||||
|
<FITID>c9d856df-339c-47c6-9f6a-8c2e2910f62e
|
||||||
|
<NAME>Dividend Earned
|
||||||
|
</STMTTRN>
|
||||||
|
<STMTTRN>
|
||||||
|
<TRNTYPE>CREDIT
|
||||||
|
<DTPOSTED>20170315120000.000[0:GMT]
|
||||||
|
<TRNAMT>3070.1328011762807
|
||||||
|
<FITID>1107ace0-048b-4c0c-b5f3-45b6be4cd71d
|
||||||
|
<NAME>Dividend Earned
|
||||||
|
</STMTTRN>
|
||||||
|
</BANKTRANLIST>
|
||||||
|
<LEDGERBAL>
|
||||||
|
<BALAMT>2607.1664944585727
|
||||||
|
<DTASOF>20170407001841.262[0:GMT]
|
||||||
|
</LEDGERBAL>
|
||||||
|
<AVAILBAL>
|
||||||
|
<BALAMT>4503.683156768119
|
||||||
|
<DTASOF>20170407001841.262[0:GMT]
|
||||||
|
</AVAILBAL>
|
||||||
|
</STMTRS>
|
||||||
|
</STMTTRNRS>
|
||||||
|
</BANKMSGSRSV1>
|
||||||
|
</OFX>
|
23
signon.go
23
signon.go
@ -9,17 +9,18 @@ import (
|
|||||||
// SignonRequest identifies and authenticates a user to their FI and is
|
// SignonRequest identifies and authenticates a user to their FI and is
|
||||||
// provided with every Request
|
// provided with every Request
|
||||||
type SignonRequest struct {
|
type SignonRequest struct {
|
||||||
XMLName xml.Name `xml:"SONRQ"`
|
XMLName xml.Name `xml:"SONRQ"`
|
||||||
DtClient Date `xml:"DTCLIENT"` // Current time on client, overwritten in Client.Request()
|
DtClient Date `xml:"DTCLIENT"` // Current time on client, overwritten in Client.Request()
|
||||||
UserID String `xml:"USERID"`
|
UserID String `xml:"USERID"`
|
||||||
UserPass String `xml:"USERPASS,omitempty"`
|
UserPass String `xml:"USERPASS,omitempty"`
|
||||||
UserKey String `xml:"USERKEY,omitempty"`
|
UserKey String `xml:"USERKEY,omitempty"`
|
||||||
Language String `xml:"LANGUAGE"` // Defaults to ENG
|
GenUserKey Boolean `xml:"GENUSERKEY,omitempty"`
|
||||||
Org String `xml:"FI>ORG"`
|
Language String `xml:"LANGUAGE"` // Defaults to ENG
|
||||||
Fid String `xml:"FI>FID"`
|
Org String `xml:"FI>ORG"`
|
||||||
AppID String `xml:"APPID"` // Overwritten in Client.Request()
|
Fid String `xml:"FI>FID"`
|
||||||
AppVer String `xml:"APPVER"` // Overwritten in Client.Request()
|
AppID String `xml:"APPID"` // Overwritten in Client.Request()
|
||||||
ClientUID UID `xml:"CLIENTUID,omitempty"`
|
AppVer String `xml:"APPVER"` // Overwritten in Client.Request()
|
||||||
|
ClientUID UID `xml:"CLIENTUID,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the name of the top-level transaction XML/SGML element
|
// Name returns the name of the top-level transaction XML/SGML element
|
||||||
|
@ -155,7 +155,7 @@ func TestAmountEqual(t *testing.T) {
|
|||||||
func TestMarshalDate(t *testing.T) {
|
func TestMarshalDate(t *testing.T) {
|
||||||
var d *Date
|
var d *Date
|
||||||
UTC := time.FixedZone("UTC", 0)
|
UTC := time.FixedZone("UTC", 0)
|
||||||
GMT_nodesc := time.FixedZone("", 0)
|
GMTNodesc := time.FixedZone("", 0)
|
||||||
EST := time.FixedZone("EST", -5*60*60)
|
EST := time.FixedZone("EST", -5*60*60)
|
||||||
NPT := time.FixedZone("NPT", (5*60+45)*60)
|
NPT := time.FixedZone("NPT", (5*60+45)*60)
|
||||||
IST := time.FixedZone("IST", (5*60+30)*60)
|
IST := time.FixedZone("IST", (5*60+30)*60)
|
||||||
@ -183,7 +183,7 @@ func TestMarshalDate(t *testing.T) {
|
|||||||
marshalHelper(t, "20170314000026.053[-3.50:NST]", d)
|
marshalHelper(t, "20170314000026.053[-3.50:NST]", d)
|
||||||
|
|
||||||
// Time zone without textual description
|
// Time zone without textual description
|
||||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT_nodesc)
|
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMTNodesc)
|
||||||
marshalHelper(t, "20170314150926.053[0]", d)
|
marshalHelper(t, "20170314150926.053[0]", d)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +195,7 @@ func TestUnmarshalDate(t *testing.T) {
|
|||||||
NPT := time.FixedZone("NPT", (5*60+45)*60)
|
NPT := time.FixedZone("NPT", (5*60+45)*60)
|
||||||
IST := time.FixedZone("IST", (5*60+30)*60)
|
IST := time.FixedZone("IST", (5*60+30)*60)
|
||||||
NST := time.FixedZone("NST", -(3*60+30)*60)
|
NST := time.FixedZone("NST", -(3*60+30)*60)
|
||||||
NST_nodesc := time.FixedZone("", -(3*60+30)*60)
|
NSTNodesc := time.FixedZone("", -(3*60+30)*60)
|
||||||
|
|
||||||
eq := func(a, b interface{}) bool {
|
eq := func(a, b interface{}) bool {
|
||||||
if dateA, ok := a.(*Date); ok {
|
if dateA, ok := a.(*Date); ok {
|
||||||
@ -245,7 +245,7 @@ func TestUnmarshalDate(t *testing.T) {
|
|||||||
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT)
|
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT)
|
||||||
unmarshalHelper2(t, "20170314150926.053[0]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053[0]", d, &overwritten, eq)
|
||||||
// but not for others:
|
// but not for others:
|
||||||
d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST_nodesc)
|
d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NSTNodesc)
|
||||||
unmarshalHelper2(t, "20170314000026.053[-3.50]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314000026.053[-3.50]", d, &overwritten, eq)
|
||||||
|
|
||||||
// Make sure we handle poorly-formatted dates (from Vanguard)
|
// Make sure we handle poorly-formatted dates (from Vanguard)
|
||||||
|
@ -15,7 +15,7 @@ type VanguardClient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewVanguardClient returns a Client interface configured to handle Vanguard's
|
// NewVanguardClient returns a Client interface configured to handle Vanguard's
|
||||||
// brand of idiosyncracy
|
// brand of idiosyncrasy
|
||||||
func NewVanguardClient(bc *BasicClient) Client {
|
func NewVanguardClient(bc *BasicClient) Client {
|
||||||
return &VanguardClient{bc}
|
return &VanguardClient{bc}
|
||||||
}
|
}
|
||||||
@ -47,6 +47,8 @@ func rawRequestCookies(URL string, r io.Reader, cookies []*http.Cookie) (*http.R
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestNoParse marshals a Request to XML, makes an HTTP request, and returns
|
||||||
|
// the raw HTTP response
|
||||||
func (c *VanguardClient) RequestNoParse(r *Request) (*http.Response, error) {
|
func (c *VanguardClient) RequestNoParse(r *Request) (*http.Response, error) {
|
||||||
r.SetClientFields(c)
|
r.SetClientFields(c)
|
||||||
|
|
||||||
@ -62,7 +64,7 @@ func (c *VanguardClient) RequestNoParse(r *Request) (*http.Response, error) {
|
|||||||
// Fortunately, the initial response contains the cookie we need, so if we
|
// Fortunately, the initial response contains the cookie we need, so if we
|
||||||
// detect an empty response with cookies set that didn't have any errors,
|
// detect an empty response with cookies set that didn't have any errors,
|
||||||
// re-try the request while sending their cookies back to them.
|
// re-try the request while sending their cookies back to them.
|
||||||
if err == nil && response.ContentLength <= 0 && len(response.Cookies()) > 0 {
|
if response != nil && response.ContentLength <= 0 && len(response.Cookies()) > 0 {
|
||||||
b, err = r.Marshal()
|
b, err = r.Marshal()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -74,6 +76,8 @@ func (c *VanguardClient) RequestNoParse(r *Request) (*http.Response, error) {
|
|||||||
return response, err
|
return response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request marshals a Request to XML, makes an HTTP request, and then
|
||||||
|
// unmarshals the response into a Response object.
|
||||||
func (c *VanguardClient) Request(r *Request) (*Response, error) {
|
func (c *VanguardClient) Request(r *Request) (*Response, error) {
|
||||||
return clientRequest(c, r)
|
return clientRequest(c, r)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user