mirror of
https://github.com/aclindsa/ofxgo.git
synced 2025-07-03 20:38:39 -04:00
Compare commits
69 Commits
Author | SHA1 | Date | |
---|---|---|---|
afd882f7d2 | |||
5ed0050aad | |||
09f161e13e | |||
e1a72fcd54 | |||
a4a166aa74 | |||
3ee400d1ec | |||
67fa945cc8 | |||
cb48d30deb | |||
12ea3b7e8b | |||
e76c697cad | |||
2641443ebe | |||
01b26887af | |||
2b8a79e4b7 | |||
9136c9bab2 | |||
0d93a42626 | |||
56ca46714b | |||
4c7c48cab7 | |||
8c1e6eafab | |||
52f3e4120b | |||
ef87cc536c | |||
830a6064c7 | |||
6807c93e0e | |||
10edd94920 | |||
d88d45a664 | |||
2caa23564a | |||
5923a34de0 | |||
aa4d8074b2 | |||
65cc26a0db | |||
8f3e7309f2 | |||
631508ccc9 | |||
60a5707de6 | |||
3240ef383b | |||
8ad638c7e2 | |||
f19189de45 | |||
677a09295a | |||
f75592381a | |||
ebf7f5b757 | |||
212fdc731b | |||
66dd37781f | |||
67e527c855 | |||
f41286cac7 | |||
423d460747 | |||
3e8a9c5a53 | |||
35c7116654 | |||
286e619071 | |||
9dd9c3bd3f | |||
0f6ceccd86 | |||
7691881132 | |||
61262b87d8 | |||
5e2e3a2bf7 | |||
22a6d65b98 | |||
77b154695f | |||
ac09538ec3 | |||
d8491bed1d | |||
1b4f27b31f | |||
eb35a26986 | |||
5c10ac5ea1 | |||
94a77ac754 | |||
de58d3fc0d | |||
88e5521348 | |||
54666608a4 | |||
1cc508c6d3 | |||
c6a806399a | |||
2a92b29a62 | |||
2fbb276a22 | |||
06de7e2af6 | |||
977dacfbbd | |||
ddc674b287 | |||
29fc9c20fe |
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.16.x, 1.17.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
|
17
.travis.yml
17
.travis.yml
@ -1,17 +0,0 @@
|
|||||||
language: go
|
|
||||||
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.9.x
|
|
||||||
- master
|
|
||||||
|
|
||||||
script:
|
|
||||||
- go get golang.org/x/tools/cmd/cover
|
|
||||||
- go get github.com/mattn/goveralls
|
|
||||||
- go install github.com/mattn/goveralls
|
|
||||||
- go test -v -covermode=count -coverprofile=coverage.out
|
|
||||||
|
|
||||||
after_script:
|
|
||||||
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN
|
|
90
README.md
90
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
|
||||||
@ -26,7 +26,8 @@ created a sample command-line client which uses the library to do simple tasks
|
|||||||
(currently it does little more than list accounts and query for balances and
|
(currently it does little more than list accounts and query for balances and
|
||||||
transactions). My hope is that by studying its code, new users will be able to
|
transactions). My hope is that by studying its code, new users will be able to
|
||||||
figure out how to use the library much faster than staring at the OFX
|
figure out how to use the library much faster than staring at the OFX
|
||||||
specification (or this library's API documentation). The command-line client
|
specification (or this library's [API
|
||||||
|
documentation](https://pkg.go.dev/github.com/aclindsa/ofxgo)). The command-line client
|
||||||
also serves as an easy way for me to test/debug the library with actual
|
also serves as an easy way for me to test/debug the library with actual
|
||||||
financial institutions, which frequently have 'quirks' in their implementations.
|
financial institutions, which frequently have 'quirks' in their implementations.
|
||||||
The command-line client can be found in the [cmd/ofx
|
The command-line client can be found in the [cmd/ofx
|
||||||
@ -36,11 +37,88 @@ 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
|
||||||
|
|
||||||
|
The following code snippet demonstrates how to use OFXGo to query and parse
|
||||||
|
OFX code from a checking account, printing the balance and returned transactions:
|
||||||
|
|
||||||
|
```go
|
||||||
|
client := ofxgo.BasicClient{} // Accept the default Client settings
|
||||||
|
|
||||||
|
// These values are specific to your bank
|
||||||
|
var query ofxgo.Request
|
||||||
|
query.URL = "https://secu.example.com/ofx"
|
||||||
|
query.Signon.Org = ofxgo.String("SECU")
|
||||||
|
query.Signon.Fid = ofxgo.String("1234")
|
||||||
|
|
||||||
|
// Set your username/password
|
||||||
|
query.Signon.UserID = ofxgo.String("username")
|
||||||
|
query.Signon.UserPass = ofxgo.String("hunter2")
|
||||||
|
|
||||||
|
uid, _ := ofxgo.RandomUID() // Handle error in real code
|
||||||
|
query.Bank = append(query.Bank, &ofxgo.StatementRequest{
|
||||||
|
TrnUID: *uid,
|
||||||
|
BankAcctFrom: ofxgo.BankAcct{
|
||||||
|
BankID: ofxgo.String("123456789"), // Possibly your routing number
|
||||||
|
AcctID: ofxgo.String("00011122233"), // Possibly your account number
|
||||||
|
AcctType: ofxgo.AcctTypeChecking,
|
||||||
|
},
|
||||||
|
Include: true, // Include transactions (instead of only balance information)
|
||||||
|
})
|
||||||
|
|
||||||
|
response, _ := client.Request(&query) // Handle error in real code
|
||||||
|
|
||||||
|
// Was there an OFX error while processing our request?
|
||||||
|
if response.Signon.Status.Code != 0 {
|
||||||
|
meaning, _ := response.Signon.Status.CodeMeaning()
|
||||||
|
fmt.Printf("Nonzero signon status (%d: %s) with message: %s\n", response.Signon.Status.Code, meaning, response.Signon.Status.Message)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(response.Bank) < 1 {
|
||||||
|
fmt.Println("No banking messages received")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stmt, ok := response.Bank[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 {
|
||||||
|
currency := stmt.CurDef
|
||||||
|
if ok, _ := tran.Currency.Valid(); ok {
|
||||||
|
currency = tran.Currency.CurSym
|
||||||
|
}
|
||||||
|
fmt.Printf("%s %-15s %-11s %s%s%s\n", tran.DtPosted, tran.TrnAmt.String()+" "+currency.String(), tran.TrnType, tran.Name, tran.Payee.Name, tran.Memo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
13
bank.go
13
bank.go
@ -126,8 +126,8 @@ type Transaction struct {
|
|||||||
ImageData []ImageData `xml:"IMAGEDATA,omitempty"`
|
ImageData []ImageData `xml:"IMAGEDATA,omitempty"`
|
||||||
|
|
||||||
// Only one of Currency and OrigCurrency can ever be Valid() for the same transaction
|
// Only one of Currency and OrigCurrency can ever be Valid() for the same transaction
|
||||||
Currency Currency `xml:"CURRENCY,omitempty"` // Represents the currency of TrnAmt (instead of CURDEF in STMTRS) if Valid
|
Currency *Currency `xml:"CURRENCY,omitempty"` // Represents the currency of TrnAmt (instead of CURDEF in STMTRS) if Valid
|
||||||
OrigCurrency Currency `xml:"ORIGCURRENCY,omitempty"` // Represents the currency TrnAmt was converted to STMTRS' CURDEF from if Valid
|
OrigCurrency *Currency `xml:"ORIGCURRENCY,omitempty"` // Represents the currency TrnAmt was converted to STMTRS' CURDEF from if Valid
|
||||||
Inv401kSource inv401kSource `xml:"INV401KSOURCE,omitempty"` // One of PRETAX, AFTERTAX, MATCH, PROFITSHARING, ROLLOVER, OTHERVEST, OTHERNONVEST (Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST.)
|
Inv401kSource inv401kSource `xml:"INV401KSOURCE,omitempty"` // One of PRETAX, AFTERTAX, MATCH, PROFITSHARING, ROLLOVER, OTHERVEST, OTHERNONVEST (Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST.)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,8 +166,13 @@ func (t Transaction) Valid(version ofxVersion) (bool, error) {
|
|||||||
} else if len(t.ImageData) > 2 {
|
} else if len(t.ImageData) > 2 {
|
||||||
return false, errors.New("Only 2 of ImageData allowed in Transaction")
|
return false, errors.New("Only 2 of ImageData allowed in Transaction")
|
||||||
}
|
}
|
||||||
ok1, _ := t.Currency.Valid()
|
var ok1, ok2 bool
|
||||||
ok2, _ := t.OrigCurrency.Valid()
|
if t.Currency != nil {
|
||||||
|
ok1, _ = t.Currency.Valid()
|
||||||
|
}
|
||||||
|
if t.OrigCurrency != nil {
|
||||||
|
ok2, _ = t.OrigCurrency.Valid()
|
||||||
|
}
|
||||||
if ok1 && ok2 {
|
if ok1 && ok2 {
|
||||||
return false, errors.New("Currency and OrigCurrency both supplied for Pending Transaction, only one allowed")
|
return false, errors.New("Currency and OrigCurrency both supplied for Pending Transaction, only one allowed")
|
||||||
}
|
}
|
||||||
|
203
bank_test.go
203
bank_test.go
@ -1,7 +1,6 @@
|
|||||||
package ofxgo_test
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -42,24 +41,24 @@ func TestMarshalBankStatementRequest(t *testing.T) {
|
|||||||
</BANKMSGSRQV1>
|
</BANKMSGSRQV1>
|
||||||
</OFX>`
|
</OFX>`
|
||||||
|
|
||||||
var client = ofxgo.Client{
|
var client = BasicClient{
|
||||||
AppID: "OFXGO",
|
AppID: "OFXGO",
|
||||||
AppVer: "0001",
|
AppVer: "0001",
|
||||||
SpecVersion: ofxgo.OfxVersion203,
|
SpecVersion: OfxVersion203,
|
||||||
}
|
}
|
||||||
|
|
||||||
var request ofxgo.Request
|
var request Request
|
||||||
request.Signon.UserID = "myusername"
|
request.Signon.UserID = "myusername"
|
||||||
request.Signon.UserPass = "Pa$$word"
|
request.Signon.UserPass = "Pa$$word"
|
||||||
request.Signon.Org = "BNK"
|
request.Signon.Org = "BNK"
|
||||||
request.Signon.Fid = "1987"
|
request.Signon.Fid = "1987"
|
||||||
|
|
||||||
statementRequest := ofxgo.StatementRequest{
|
statementRequest := StatementRequest{
|
||||||
TrnUID: "123",
|
TrnUID: "123",
|
||||||
BankAcctFrom: ofxgo.BankAcct{
|
BankAcctFrom: BankAcct{
|
||||||
BankID: "318398732",
|
BankID: "318398732",
|
||||||
AcctID: "78346129",
|
AcctID: "78346129",
|
||||||
AcctType: ofxgo.AcctTypeChecking,
|
AcctType: AcctTypeChecking,
|
||||||
},
|
},
|
||||||
Include: true,
|
Include: true,
|
||||||
}
|
}
|
||||||
@ -68,7 +67,7 @@ func TestMarshalBankStatementRequest(t *testing.T) {
|
|||||||
request.SetClientFields(&client)
|
request.SetClientFields(&client)
|
||||||
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
||||||
EST := time.FixedZone("EST", -5*60*60)
|
EST := time.FixedZone("EST", -5*60*60)
|
||||||
request.Signon.DtClient = *ofxgo.NewDate(2006, 1, 15, 11, 23, 0, 0, EST)
|
request.Signon.DtClient = *NewDate(2006, 1, 15, 11, 23, 0, 0, EST)
|
||||||
|
|
||||||
marshalCheckRequest(t, &request, expectedString)
|
marshalCheckRequest(t, &request, expectedString)
|
||||||
}
|
}
|
||||||
@ -87,53 +86,53 @@ NEWFILEUID:NONE
|
|||||||
<OFX>
|
<OFX>
|
||||||
<SIGNONMSGSRQV1>
|
<SIGNONMSGSRQV1>
|
||||||
<SONRQ>
|
<SONRQ>
|
||||||
<DTCLIENT>20060115112300.000[-5:EST]</DTCLIENT>
|
<DTCLIENT>20060115112300.000[-5:EST]
|
||||||
<USERID>myusername</USERID>
|
<USERID>myusername
|
||||||
<USERPASS>Pa$$word</USERPASS>
|
<USERPASS>Pa$$word
|
||||||
<LANGUAGE>ENG</LANGUAGE>
|
<LANGUAGE>ENG
|
||||||
<FI>
|
<FI>
|
||||||
<ORG>BNK</ORG>
|
<ORG>BNK
|
||||||
<FID>1987</FID>
|
<FID>1987
|
||||||
</FI>
|
</FI>
|
||||||
<APPID>OFXGO</APPID>
|
<APPID>OFXGO
|
||||||
<APPVER>0001</APPVER>
|
<APPVER>0001
|
||||||
</SONRQ>
|
</SONRQ>
|
||||||
</SIGNONMSGSRQV1>
|
</SIGNONMSGSRQV1>
|
||||||
<BANKMSGSRQV1>
|
<BANKMSGSRQV1>
|
||||||
<STMTTRNRQ>
|
<STMTTRNRQ>
|
||||||
<TRNUID>123</TRNUID>
|
<TRNUID>123
|
||||||
<STMTRQ>
|
<STMTRQ>
|
||||||
<BANKACCTFROM>
|
<BANKACCTFROM>
|
||||||
<BANKID>318398732</BANKID>
|
<BANKID>318398732
|
||||||
<ACCTID>78346129</ACCTID>
|
<ACCTID>78346129
|
||||||
<ACCTTYPE>CHECKING</ACCTTYPE>
|
<ACCTTYPE>CHECKING
|
||||||
</BANKACCTFROM>
|
</BANKACCTFROM>
|
||||||
<INCTRAN>
|
<INCTRAN>
|
||||||
<INCLUDE>Y</INCLUDE>
|
<INCLUDE>Y
|
||||||
</INCTRAN>
|
</INCTRAN>
|
||||||
</STMTRQ>
|
</STMTRQ>
|
||||||
</STMTTRNRQ>
|
</STMTTRNRQ>
|
||||||
</BANKMSGSRQV1>
|
</BANKMSGSRQV1>
|
||||||
</OFX>`
|
</OFX>`
|
||||||
|
|
||||||
var client = ofxgo.Client{
|
var client = BasicClient{
|
||||||
AppID: "OFXGO",
|
AppID: "OFXGO",
|
||||||
AppVer: "0001",
|
AppVer: "0001",
|
||||||
SpecVersion: ofxgo.OfxVersion103,
|
SpecVersion: OfxVersion103,
|
||||||
}
|
}
|
||||||
|
|
||||||
var request ofxgo.Request
|
var request Request
|
||||||
request.Signon.UserID = "myusername"
|
request.Signon.UserID = "myusername"
|
||||||
request.Signon.UserPass = "Pa$$word"
|
request.Signon.UserPass = "Pa$$word"
|
||||||
request.Signon.Org = "BNK"
|
request.Signon.Org = "BNK"
|
||||||
request.Signon.Fid = "1987"
|
request.Signon.Fid = "1987"
|
||||||
|
|
||||||
statementRequest := ofxgo.StatementRequest{
|
statementRequest := StatementRequest{
|
||||||
TrnUID: "123",
|
TrnUID: "123",
|
||||||
BankAcctFrom: ofxgo.BankAcct{
|
BankAcctFrom: BankAcct{
|
||||||
BankID: "318398732",
|
BankID: "318398732",
|
||||||
AcctID: "78346129",
|
AcctID: "78346129",
|
||||||
AcctType: ofxgo.AcctTypeChecking,
|
AcctType: AcctTypeChecking,
|
||||||
},
|
},
|
||||||
Include: true,
|
Include: true,
|
||||||
}
|
}
|
||||||
@ -142,7 +141,7 @@ NEWFILEUID:NONE
|
|||||||
request.SetClientFields(&client)
|
request.SetClientFields(&client)
|
||||||
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
||||||
EST := time.FixedZone("EST", -5*60*60)
|
EST := time.FixedZone("EST", -5*60*60)
|
||||||
request.Signon.DtClient = *ofxgo.NewDate(2006, 1, 15, 11, 23, 0, 0, EST)
|
request.Signon.DtClient = *NewDate(2006, 1, 15, 11, 23, 0, 0, EST)
|
||||||
|
|
||||||
marshalCheckRequest(t, &request, expectedString)
|
marshalCheckRequest(t, &request, expectedString)
|
||||||
}
|
}
|
||||||
@ -211,76 +210,174 @@ func TestUnmarshalBankStatementResponse(t *testing.T) {
|
|||||||
</STMTTRNRS>
|
</STMTTRNRS>
|
||||||
</BANKMSGSRSV1>
|
</BANKMSGSRSV1>
|
||||||
</OFX>`)
|
</OFX>`)
|
||||||
var expected ofxgo.Response
|
var expected Response
|
||||||
|
|
||||||
expected.Version = ofxgo.OfxVersion203
|
expected.Version = OfxVersion203
|
||||||
expected.Signon.Status.Code = 0
|
expected.Signon.Status.Code = 0
|
||||||
expected.Signon.Status.Severity = "INFO"
|
expected.Signon.Status.Severity = "INFO"
|
||||||
expected.Signon.DtServer = *ofxgo.NewDateGMT(2006, 1, 15, 11, 23, 03, 0)
|
expected.Signon.DtServer = *NewDateGMT(2006, 1, 15, 11, 23, 03, 0)
|
||||||
expected.Signon.Language = "ENG"
|
expected.Signon.Language = "ENG"
|
||||||
expected.Signon.DtProfUp = ofxgo.NewDateGMT(2005, 2, 21, 9, 13, 0, 0)
|
expected.Signon.DtProfUp = NewDateGMT(2005, 2, 21, 9, 13, 0, 0)
|
||||||
expected.Signon.DtAcctUp = ofxgo.NewDateGMT(2006, 1, 2, 16, 0, 0, 0)
|
expected.Signon.DtAcctUp = NewDateGMT(2006, 1, 2, 16, 0, 0, 0)
|
||||||
expected.Signon.Org = "BNK"
|
expected.Signon.Org = "BNK"
|
||||||
expected.Signon.Fid = "1987"
|
expected.Signon.Fid = "1987"
|
||||||
|
|
||||||
var trnamt1, trnamt2 ofxgo.Amount
|
var trnamt1, trnamt2 Amount
|
||||||
trnamt1.SetFrac64(-20000, 100)
|
trnamt1.SetFrac64(-20000, 100)
|
||||||
trnamt2.SetFrac64(-30000, 100)
|
trnamt2.SetFrac64(-30000, 100)
|
||||||
|
|
||||||
banktranlist := ofxgo.TransactionList{
|
banktranlist := TransactionList{
|
||||||
DtStart: *ofxgo.NewDateGMT(2006, 1, 1, 0, 0, 0, 0),
|
DtStart: *NewDateGMT(2006, 1, 1, 0, 0, 0, 0),
|
||||||
DtEnd: *ofxgo.NewDateGMT(2006, 1, 15, 0, 0, 0, 0),
|
DtEnd: *NewDateGMT(2006, 1, 15, 0, 0, 0, 0),
|
||||||
Transactions: []ofxgo.Transaction{
|
Transactions: []Transaction{
|
||||||
{
|
{
|
||||||
TrnType: ofxgo.TrnTypeCheck,
|
TrnType: TrnTypeCheck,
|
||||||
DtPosted: *ofxgo.NewDateGMT(2006, 1, 4, 0, 0, 0, 0),
|
DtPosted: *NewDateGMT(2006, 1, 4, 0, 0, 0, 0),
|
||||||
TrnAmt: trnamt1,
|
TrnAmt: trnamt1,
|
||||||
FiTID: "00592",
|
FiTID: "00592",
|
||||||
CheckNum: "2002",
|
CheckNum: "2002",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TrnType: ofxgo.TrnTypeATM,
|
TrnType: TrnTypeATM,
|
||||||
DtPosted: *ofxgo.NewDateGMT(2006, 1, 12, 0, 0, 0, 0),
|
DtPosted: *NewDateGMT(2006, 1, 12, 0, 0, 0, 0),
|
||||||
DtUser: ofxgo.NewDateGMT(2006, 1, 12, 0, 0, 0, 0),
|
DtUser: NewDateGMT(2006, 1, 12, 0, 0, 0, 0),
|
||||||
TrnAmt: trnamt2,
|
TrnAmt: trnamt2,
|
||||||
FiTID: "00679",
|
FiTID: "00679",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var balamt, availbalamt ofxgo.Amount
|
var balamt, availbalamt Amount
|
||||||
balamt.SetFrac64(20029, 100)
|
balamt.SetFrac64(20029, 100)
|
||||||
availbalamt.SetFrac64(20029, 100)
|
availbalamt.SetFrac64(20029, 100)
|
||||||
|
|
||||||
usd, err := ofxgo.NewCurrSymbol("USD")
|
usd, err := NewCurrSymbol("USD")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating CurrSymbol for USD\n")
|
t.Fatalf("Unexpected error creating CurrSymbol for USD\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
statementResponse := ofxgo.StatementResponse{
|
statementResponse := StatementResponse{
|
||||||
TrnUID: "1001",
|
TrnUID: "1001",
|
||||||
Status: ofxgo.Status{
|
Status: Status{
|
||||||
Code: 0,
|
Code: 0,
|
||||||
Severity: "INFO",
|
Severity: "INFO",
|
||||||
},
|
},
|
||||||
CurDef: *usd,
|
CurDef: *usd,
|
||||||
BankAcctFrom: ofxgo.BankAcct{
|
BankAcctFrom: BankAcct{
|
||||||
BankID: "318398732",
|
BankID: "318398732",
|
||||||
AcctID: "78346129",
|
AcctID: "78346129",
|
||||||
AcctType: ofxgo.AcctTypeChecking,
|
AcctType: AcctTypeChecking,
|
||||||
},
|
},
|
||||||
BankTranList: &banktranlist,
|
BankTranList: &banktranlist,
|
||||||
BalAmt: balamt,
|
BalAmt: balamt,
|
||||||
DtAsOf: *ofxgo.NewDateGMT(2006, 1, 14, 16, 0, 0, 0),
|
DtAsOf: *NewDateGMT(2006, 1, 14, 16, 0, 0, 0),
|
||||||
AvailBalAmt: &availbalamt,
|
AvailBalAmt: &availbalamt,
|
||||||
AvailDtAsOf: ofxgo.NewDateGMT(2006, 1, 14, 16, 0, 0, 0),
|
AvailDtAsOf: NewDateGMT(2006, 1, 14, 16, 0, 0, 0),
|
||||||
}
|
}
|
||||||
expected.Bank = append(expected.Bank, &statementResponse)
|
expected.Bank = append(expected.Bank, &statementResponse)
|
||||||
|
|
||||||
response, err := ofxgo.ParseResponse(responseReader)
|
response, err := ParseResponse(responseReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkResponsesEqual(t, &expected, response)
|
checkResponsesEqual(t, &expected, response)
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayeeValid(t *testing.T) {
|
||||||
|
p := Payee{
|
||||||
|
Name: "Jane",
|
||||||
|
Addr1: "Sesame Street",
|
||||||
|
City: "Mytown",
|
||||||
|
State: "AA",
|
||||||
|
PostalCode: "12345",
|
||||||
|
Phone: "12345678901",
|
||||||
|
}
|
||||||
|
valid, err := p.Valid()
|
||||||
|
if !valid {
|
||||||
|
t.Fatalf("Unexpected error from calling Valid: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure some empty fields trigger invalid response
|
||||||
|
badp := p
|
||||||
|
badp.Name = ""
|
||||||
|
valid, err = badp.Valid()
|
||||||
|
if valid || err == nil {
|
||||||
|
t.Fatalf("Expected error from calling Valid with empty name\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
badp = p
|
||||||
|
badp.Addr1 = ""
|
||||||
|
valid, err = badp.Valid()
|
||||||
|
if valid || err == nil {
|
||||||
|
t.Fatalf("Expected error from calling Valid with empty address\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
badp = p
|
||||||
|
badp.City = ""
|
||||||
|
valid, err = badp.Valid()
|
||||||
|
if valid || err == nil {
|
||||||
|
t.Fatalf("Expected error from calling Valid with empty city\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
badp = p
|
||||||
|
badp.State = ""
|
||||||
|
valid, err = badp.Valid()
|
||||||
|
if valid || err == nil {
|
||||||
|
t.Fatalf("Expected error from calling Valid with empty state\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
badp = p
|
||||||
|
badp.PostalCode = ""
|
||||||
|
valid, err = badp.Valid()
|
||||||
|
if valid || err == nil {
|
||||||
|
t.Fatalf("Expected error from calling Valid with empty postal code\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
badp = p
|
||||||
|
badp.Phone = ""
|
||||||
|
valid, err = badp.Valid()
|
||||||
|
if valid || err == nil {
|
||||||
|
t.Fatalf("Expected error from calling Valid with empty phone\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBalanceValid(t *testing.T) {
|
||||||
|
var a Amount
|
||||||
|
a.SetFrac64(8, 1)
|
||||||
|
b := Balance{
|
||||||
|
Name: "Checking",
|
||||||
|
Desc: "Jane's Personal Checking",
|
||||||
|
BalType: BalTypeDollar,
|
||||||
|
Value: a,
|
||||||
|
}
|
||||||
|
valid, err := b.Valid()
|
||||||
|
if !valid {
|
||||||
|
t.Fatalf("Unexpected error from calling Valid: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
badb := b
|
||||||
|
badb.Name = ""
|
||||||
|
valid, err = badb.Valid()
|
||||||
|
if valid || err == nil {
|
||||||
|
t.Fatalf("Expected error from calling Valid with empty name\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
badb = b
|
||||||
|
badb.Desc = ""
|
||||||
|
valid, err = badb.Valid()
|
||||||
|
if valid || err == nil {
|
||||||
|
t.Fatalf("Expected error from calling Valid with empty description\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
badb = Balance{
|
||||||
|
Name: "Checking",
|
||||||
|
Desc: "Jane's Personal Checking",
|
||||||
|
Value: a,
|
||||||
|
}
|
||||||
|
valid, err = badb.Valid()
|
||||||
|
if valid || err == nil {
|
||||||
|
t.Fatalf("Expected error from calling Valid with unspecified balance type\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
104
basic_client.go
Normal file
104
basic_client.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package ofxgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BasicClient provides a standard Client implementation suitable for most
|
||||||
|
// financial institutions. BasicClient uses default, non-zero settings, even if
|
||||||
|
// its fields are not initialized.
|
||||||
|
type BasicClient struct {
|
||||||
|
// Request fields to overwrite with the client's values. If nonempty,
|
||||||
|
// defaults are used
|
||||||
|
SpecVersion ofxVersion // VERSION in header
|
||||||
|
AppID string // SONRQ>APPID
|
||||||
|
AppVer string // SONRQ>APPVER
|
||||||
|
|
||||||
|
// Don't insert newlines or indentation when marshalling to SGML/XML
|
||||||
|
NoIndent bool
|
||||||
|
// Use carriage returns on new lines
|
||||||
|
CarriageReturn bool
|
||||||
|
// Set User-Agent header to this string, if not empty
|
||||||
|
UserAgent string
|
||||||
|
}
|
||||||
|
|
||||||
|
// OfxVersion returns the OFX specification version this BasicClient will marshal
|
||||||
|
// Requests as. Defaults to "203" if the client's SpecVersion field is empty.
|
||||||
|
func (c *BasicClient) OfxVersion() ofxVersion {
|
||||||
|
if c.SpecVersion.Valid() {
|
||||||
|
return c.SpecVersion
|
||||||
|
}
|
||||||
|
return OfxVersion203
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns this BasicClient's OFX AppID field, defaulting to "OFXGO" if
|
||||||
|
// unspecified.
|
||||||
|
func (c *BasicClient) ID() String {
|
||||||
|
if len(c.AppID) > 0 {
|
||||||
|
return String(c.AppID)
|
||||||
|
}
|
||||||
|
return String("OFXGO")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns this BasicClient's version number as a string, defaulting to
|
||||||
|
// "0001" if unspecified.
|
||||||
|
func (c *BasicClient) Version() String {
|
||||||
|
if len(c.AppVer) > 0 {
|
||||||
|
return String(c.AppVer)
|
||||||
|
}
|
||||||
|
return String("0001")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndentRequests returns true if the marshaled XML should be indented (and
|
||||||
|
// contain newlines, since the two are linked in the current implementation)
|
||||||
|
func (c *BasicClient) IndentRequests() bool {
|
||||||
|
return !c.NoIndent
|
||||||
|
}
|
||||||
|
|
||||||
|
// CarriageReturnNewLines returns true if carriage returns should be used on new lines, false otherwise
|
||||||
|
func (c *BasicClient) CarriageReturnNewLines() bool {
|
||||||
|
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) {
|
||||||
|
if !strings.HasPrefix(URL, "https://") {
|
||||||
|
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequest("POST", URL, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
request.Header.Set("Content-Type", "application/x-ofx")
|
||||||
|
request.Header.Add("Accept", "*/*, application/x-ofx")
|
||||||
|
if c.UserAgent != "" {
|
||||||
|
request.Header.Set("User-Agent", c.UserAgent)
|
||||||
|
}
|
||||||
|
response, err := http.DefaultClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return response, errors.New("OFXQuery request status: " + response.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
return clientRequest(c, r)
|
||||||
|
}
|
192
client.go
192
client.go
@ -1,127 +1,83 @@
|
|||||||
package ofxgo
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client serves to aggregate OFX client settings that may be necessary to talk
|
// Client serves to aggregate OFX client settings that may be necessary to talk
|
||||||
// to a particular server due to quirks in that server's implementation. Client
|
// to a particular server due to quirks in that server's implementation.
|
||||||
// also provides the Request, RequestNoParse, and RawRequest helper methods to
|
// Client also provides the Request and RequestNoParse helper methods to aid in
|
||||||
// aid in making and parsing requests. Client uses default, non-zero settings,
|
// making and parsing requests.
|
||||||
// even if its fields are not initialized.
|
type Client interface {
|
||||||
type Client struct {
|
// Used to fill out a Request object
|
||||||
// Request fields to overwrite with the client's values. If nonempty,
|
OfxVersion() ofxVersion
|
||||||
// defaults are used
|
ID() String
|
||||||
SpecVersion ofxVersion // VERSION in header
|
Version() String
|
||||||
AppID string // SONRQ>APPID
|
IndentRequests() bool
|
||||||
AppVer string // SONRQ>APPVER
|
CarriageReturnNewLines() bool
|
||||||
|
|
||||||
// Don't insert newlines or indentation when marshalling to SGML/XML
|
// Request marshals a Request object into XML, makes an HTTP request
|
||||||
NoIndent bool
|
// against it's URL, and then unmarshals the response into a Response
|
||||||
}
|
// object.
|
||||||
|
//
|
||||||
|
// Before being marshaled, some of the the Request object's values are
|
||||||
|
// overwritten, namely those dictated by the BasicClient's configuration
|
||||||
|
// (Version, AppID, AppVer fields), and the client's current time
|
||||||
|
// (DtClient). These are updated in place in the supplied Request object so
|
||||||
|
// they may later be inspected by the caller.
|
||||||
|
Request(r *Request) (*Response, error)
|
||||||
|
|
||||||
var defaultClient Client
|
// RequestNoParse marshals a Request object into XML, makes an HTTP
|
||||||
|
// request, and returns the raw HTTP response. Unlike RawRequest(), it
|
||||||
// OfxVersion returns the OFX specification version this Client will marshal
|
// takes client settings into account. Unlike Request(), it doesn't parse
|
||||||
// Requests as. Defaults to "203" if the client's SpecVersion field is empty.
|
// the response into an ofxgo.Request object.
|
||||||
func (c *Client) OfxVersion() ofxVersion {
|
//
|
||||||
if c.SpecVersion.Valid() {
|
// Caveat: The caller is responsible for closing the http Response.Body
|
||||||
return c.SpecVersion
|
// (see the http module's documentation for more information)
|
||||||
}
|
RequestNoParse(r *Request) (*http.Response, error)
|
||||||
return OfxVersion203
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns this Client's OFX AppID field, defaulting to "OFXGO" if
|
|
||||||
// unspecified.
|
|
||||||
func (c *Client) ID() String {
|
|
||||||
if len(c.AppID) > 0 {
|
|
||||||
return String(c.AppID)
|
|
||||||
}
|
|
||||||
return String("OFXGO")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version returns this Client's version number as a string, defaulting to
|
|
||||||
// "0001" if unspecified.
|
|
||||||
func (c *Client) Version() String {
|
|
||||||
if len(c.AppVer) > 0 {
|
|
||||||
return String(c.AppVer)
|
|
||||||
}
|
|
||||||
return String("0001")
|
|
||||||
}
|
|
||||||
|
|
||||||
// IndentRequests returns true if the marshaled XML should be indented (and
|
|
||||||
// contain newlines, since the two are linked in the current implementation)
|
|
||||||
func (c *Client) IndentRequests() bool {
|
|
||||||
return !c.NoIndent
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawRequest is little more than a thin wrapper around http.Post
|
// RawRequest is little more than a thin wrapper around http.Post
|
||||||
//
|
//
|
||||||
// In most cases, you should probably be using Request() instead, but
|
// In most cases, you should probably be using Request() instead, but
|
||||||
// RawRequest can be useful if you need to read the raw unparsed http response
|
// RawRequest can be useful if you need to read the raw unparsed http
|
||||||
// yourself (perhaps for downloading an OFX file for use by an external
|
// response yourself (perhaps for downloading an OFX file for use by an
|
||||||
// program, or debugging server behavior), or have a handcrafted request you'd
|
// external program, or debugging server behavior), or have a handcrafted
|
||||||
// like to try.
|
// request you'd like to try.
|
||||||
//
|
//
|
||||||
// Caveats: RawRequest does *not* take client settings into account as
|
// Caveats: RawRequest does *not* take client settings into account as
|
||||||
// Request() does, so your particular server may or may not like whatever we
|
// Client.Request() does, so your particular server may or may not like
|
||||||
// read from 'r'. The caller is responsible for closing the http Response.Body
|
// whatever we read from 'r'. The caller is responsible for closing the
|
||||||
// (see the http module's documentation for more information)
|
// http Response.Body (see the http module's documentation for more
|
||||||
func RawRequest(URL string, r io.Reader) (*http.Response, error) {
|
// information)
|
||||||
if !strings.HasPrefix(URL, "https://") {
|
RawRequest(URL string, r io.Reader) (*http.Response, error)
|
||||||
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)
|
type clientCreationFunc func(*BasicClient) Client
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
// GetClient returns a new Client for a given URL. It attempts to find a
|
||||||
|
// specialized client for this URL, but simply returns the passed-in
|
||||||
|
// BasicClient if no such match is found.
|
||||||
|
func GetClient(URL string, bc *BasicClient) Client {
|
||||||
|
clients := []struct {
|
||||||
|
URL string
|
||||||
|
Func clientCreationFunc
|
||||||
|
}{
|
||||||
|
{"https://ofx.discovercard.com", NewDiscoverCardClient},
|
||||||
|
{"https://vesnc.vanguard.com/us/OfxDirectConnectServlet", NewVanguardClient},
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.URL == strings.Trim(URL, "/") {
|
||||||
|
return client.Func(bc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bc
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.StatusCode != 200 {
|
// clientRequestNoParse can be used for building clients' RequestNoParse
|
||||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
// methods if they require fairly standard behavior
|
||||||
}
|
func clientRequestNoParse(c Client, r *Request) (*http.Response, error) {
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawRequestCookies is RawRequest with the added feature of sending cookies
|
|
||||||
func RawRequestCookies(URL string, r io.Reader, cookies []*http.Cookie) (*http.Response, error) {
|
|
||||||
if !strings.HasPrefix(URL, "https://") {
|
|
||||||
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
|
||||||
}
|
|
||||||
|
|
||||||
request, err := http.NewRequest("POST", URL, r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
request.Header.Set("Content-Type", "application/x-ofx")
|
|
||||||
for _, cookie := range cookies {
|
|
||||||
request.AddCookie(cookie)
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := http.DefaultClient.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.StatusCode != 200 {
|
|
||||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestNoParse marshals a Request object into XML, makes an HTTP request,
|
|
||||||
// and returns the raw HTTP response. Unlike RawRequest(), it takes client
|
|
||||||
// settings into account. Unlike Request(), it doesn't parse the response into
|
|
||||||
// an ofxgo.Request object.
|
|
||||||
//
|
|
||||||
// Caveat: The caller is responsible for closing the http Response.Body (see
|
|
||||||
// the http module's documentation for more information)
|
|
||||||
func (c *Client) RequestNoParse(r *Request) (*http.Response, error) {
|
|
||||||
r.SetClientFields(c)
|
r.SetClientFields(c)
|
||||||
|
|
||||||
b, err := r.Marshal()
|
b, err := r.Marshal()
|
||||||
@ -129,34 +85,12 @@ func (c *Client) RequestNoParse(r *Request) (*http.Response, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := RawRequest(r.URL, b)
|
return c.RawRequest(r.URL, b)
|
||||||
|
|
||||||
// Some financial institutions (cough, Vanguard, cough), require a cookie
|
|
||||||
// to be set on the http request, or they return empty responses.
|
|
||||||
// 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,
|
|
||||||
// re-try the request while sending their cookies back to them.
|
|
||||||
if err == nil && response.ContentLength <= 0 && len(response.Cookies()) > 0 {
|
|
||||||
b, err = r.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return RawRequestCookies(r.URL, b, response.Cookies())
|
// clientRequest can be used for building clients' Request methods if they
|
||||||
}
|
// require fairly standard behavior
|
||||||
|
func clientRequest(c Client, r *Request) (*Response, error) {
|
||||||
return response, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request marshals a Request object into XML, makes an HTTP request against
|
|
||||||
// it's URL, and then unmarshals the response into a Response object.
|
|
||||||
//
|
|
||||||
// Before being marshaled, some of the the Request object's values are
|
|
||||||
// overwritten, namely those dictated by the Client's configuration (Version,
|
|
||||||
// AppID, AppVer fields), and the client's curren time (DtClient). These are
|
|
||||||
// updated in place in the supplied Request object so they may later be
|
|
||||||
// inspected by the caller.
|
|
||||||
func (c *Client) Request(r *Request) (*Response, error) {
|
|
||||||
response, err := c.RequestNoParse(r)
|
response, err := c.RequestNoParse(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -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)")
|
||||||
|
@ -77,7 +77,7 @@ func bankTransactions() {
|
|||||||
|
|
||||||
func printTransaction(defCurrency ofxgo.CurrSymbol, tran *ofxgo.Transaction) {
|
func printTransaction(defCurrency ofxgo.CurrSymbol, tran *ofxgo.Transaction) {
|
||||||
currency := defCurrency
|
currency := defCurrency
|
||||||
if ok, _ := tran.Currency.Valid(); ok {
|
if tran.Currency != nil {
|
||||||
currency = tran.Currency.CurSym
|
currency = tran.Currency.CurSym
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ func ccTransactions() {
|
|||||||
fmt.Println("Transactions:")
|
fmt.Println("Transactions:")
|
||||||
for _, tran := range stmt.BankTranList.Transactions {
|
for _, tran := range stmt.BankTranList.Transactions {
|
||||||
currency := stmt.CurDef
|
currency := stmt.CurDef
|
||||||
if ok, _ := tran.Currency.Valid(); ok {
|
if tran.Currency != nil {
|
||||||
currency = tran.Currency.CurSym
|
currency = tran.Currency.CurSym
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/howeyc/gopass"
|
"golang.org/x/term"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
type command struct {
|
type command struct {
|
||||||
@ -22,6 +23,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 +38,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 {
|
||||||
@ -49,7 +56,7 @@ func checkServerFlags() bool {
|
|||||||
|
|
||||||
if ret && len(password) == 0 {
|
if ret && len(password) == 0 {
|
||||||
fmt.Printf("Password for %s: ", username)
|
fmt.Printf("Password for %s: ", username)
|
||||||
pass, err := gopass.GetPasswd()
|
pass, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error reading password: %s\n", err)
|
fmt.Printf("Error reading password: %s\n", err)
|
||||||
ret = false
|
ret = false
|
||||||
|
@ -43,6 +43,8 @@ var appVersions = map[string][]string{
|
|||||||
"0001",
|
"0001",
|
||||||
},
|
},
|
||||||
"QWIN": { // Intuit Quicken Windows
|
"QWIN": { // Intuit Quicken Windows
|
||||||
|
"2600", // 2017
|
||||||
|
"2500", // 2016
|
||||||
"2400", // 2015
|
"2400", // 2015
|
||||||
"2300", // 2014
|
"2300", // 2014
|
||||||
"2200", // 2013
|
"2200", // 2013
|
||||||
@ -126,12 +128,13 @@ func tryProfile(appID, appVer, version string, noindent bool) bool {
|
|||||||
fmt.Println("Error creating new OfxVersion enum:", err)
|
fmt.Println("Error creating new OfxVersion enum:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
var client = ofxgo.Client{
|
var client = ofxgo.GetClient(serverURL,
|
||||||
|
&ofxgo.BasicClient{
|
||||||
AppID: appID,
|
AppID: appID,
|
||||||
AppVer: appVer,
|
AppVer: appVer,
|
||||||
SpecVersion: ver,
|
SpecVersion: ver,
|
||||||
NoIndent: noindent,
|
NoIndent: noindent,
|
||||||
}
|
})
|
||||||
|
|
||||||
var query ofxgo.Request
|
var query ofxgo.Request
|
||||||
query.URL = serverURL
|
query.URL = serverURL
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -6,18 +6,21 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newRequest() (*ofxgo.Client, *ofxgo.Request) {
|
func newRequest() (ofxgo.Client, *ofxgo.Request) {
|
||||||
ver, err := ofxgo.NewOfxVersion(ofxVersion)
|
ver, err := ofxgo.NewOfxVersion(ofxVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error creating new OfxVersion enum:", err)
|
fmt.Println("Error creating new OfxVersion enum:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
var client = ofxgo.Client{
|
var client = ofxgo.GetClient(serverURL,
|
||||||
|
&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
|
||||||
query.URL = serverURL
|
query.URL = serverURL
|
||||||
@ -27,5 +30,16 @@ func newRequest() (*ofxgo.Client, *ofxgo.Request) {
|
|||||||
query.Signon.Org = ofxgo.String(org)
|
query.Signon.Org = ofxgo.String(org)
|
||||||
query.Signon.Fid = ofxgo.String(fid)
|
query.Signon.Fid = ofxgo.String(fid)
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
40
common.go
40
common.go
@ -3,10 +3,50 @@ package ofxgo
|
|||||||
//go:generate ./generate_constants.py
|
//go:generate ./generate_constants.py
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/aclindsa/xml"
|
"github.com/aclindsa/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func writeHeader(b *bytes.Buffer, v ofxVersion, carriageReturn bool) error {
|
||||||
|
// Write the header appropriate to our version
|
||||||
|
switch v {
|
||||||
|
case OfxVersion102, OfxVersion103, OfxVersion151, OfxVersion160:
|
||||||
|
header := `OFXHEADER:100
|
||||||
|
DATA:OFXSGML
|
||||||
|
VERSION:` + v.String() + `
|
||||||
|
SECURITY:NONE
|
||||||
|
ENCODING:USASCII
|
||||||
|
CHARSET:1252
|
||||||
|
COMPRESSION:NONE
|
||||||
|
OLDFILEUID:NONE
|
||||||
|
NEWFILEUID:NONE
|
||||||
|
|
||||||
|
`
|
||||||
|
if carriageReturn {
|
||||||
|
header = strings.Replace(header, "\n", "\r\n", -1)
|
||||||
|
}
|
||||||
|
b.WriteString(header)
|
||||||
|
case OfxVersion200, OfxVersion201, OfxVersion202, OfxVersion203, OfxVersion210, OfxVersion211, OfxVersion220:
|
||||||
|
b.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>`)
|
||||||
|
if carriageReturn {
|
||||||
|
b.WriteByte('\r')
|
||||||
|
}
|
||||||
|
b.WriteByte('\n')
|
||||||
|
b.WriteString(`<?OFX OFXHEADER="200" VERSION="` + v.String() + `" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>`)
|
||||||
|
if carriageReturn {
|
||||||
|
b.WriteByte('\r')
|
||||||
|
}
|
||||||
|
b.WriteByte('\n')
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%d is not a valid OFX version string", v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Message represents an OFX message in a message set. it is used to ease
|
// Message represents an OFX message in a message set. it is used to ease
|
||||||
// marshalling and unmarshalling.
|
// marshalling and unmarshalling.
|
||||||
type Message interface {
|
type Message interface {
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
package ofxgo_test
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStatusValid(t *testing.T) {
|
func TestStatusValid(t *testing.T) {
|
||||||
s := ofxgo.Status{
|
s := Status{
|
||||||
Code: 0,
|
Code: 0,
|
||||||
Severity: "INFO",
|
Severity: "INFO",
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
@ -32,7 +31,7 @@ func TestStatusValid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusCodeMeaning(t *testing.T) {
|
func TestStatusCodeMeaning(t *testing.T) {
|
||||||
s := ofxgo.Status{
|
s := Status{
|
||||||
Code: 15500,
|
Code: 15500,
|
||||||
Severity: "ERROR",
|
Severity: "ERROR",
|
||||||
}
|
}
|
||||||
@ -51,7 +50,7 @@ func TestStatusCodeMeaning(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusCodeConditions(t *testing.T) {
|
func TestStatusCodeConditions(t *testing.T) {
|
||||||
s := ofxgo.Status{
|
s := Status{
|
||||||
Code: 2006,
|
Code: 2006,
|
||||||
Severity: "ERROR",
|
Severity: "ERROR",
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,9 @@ package ofxgo
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/aclindsa/xml"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aclindsa/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ofxVersion uint
|
type ofxVersion uint
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package ofxgo_test
|
package ofxgo
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Do not edit this file by hand. It is auto-generated by calling `go generate`.
|
* Do not edit this file by hand. It is auto-generated by calling `go generate`.
|
||||||
@ -7,14 +7,14 @@ package ofxgo_test
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aclindsa/xml"
|
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aclindsa/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOfxVersion(t *testing.T) {
|
func TestOfxVersion(t *testing.T) {
|
||||||
e, err := ofxgo.NewOfxVersion("102")
|
e, err := NewOfxVersion("102")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new OfxVersion from string \"102\"\n")
|
t.Fatalf("Unexpected error creating new OfxVersion from string \"102\"\n")
|
||||||
}
|
}
|
||||||
@ -31,7 +31,7 @@ func TestOfxVersion(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "220", &e)
|
marshalHelper(t, "220", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewOfxVersion("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewOfxVersion("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new OfxVersion from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new OfxVersion from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -59,7 +59,7 @@ func TestOfxVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAcctType(t *testing.T) {
|
func TestAcctType(t *testing.T) {
|
||||||
e, err := ofxgo.NewAcctType("CHECKING")
|
e, err := NewAcctType("CHECKING")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new AcctType from string \"CHECKING\"\n")
|
t.Fatalf("Unexpected error creating new AcctType from string \"CHECKING\"\n")
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ func TestAcctType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "CD", &e)
|
marshalHelper(t, "CD", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewAcctType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewAcctType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new AcctType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new AcctType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -104,7 +104,7 @@ func TestAcctType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTrnType(t *testing.T) {
|
func TestTrnType(t *testing.T) {
|
||||||
e, err := ofxgo.NewTrnType("CREDIT")
|
e, err := NewTrnType("CREDIT")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new TrnType from string \"CREDIT\"\n")
|
t.Fatalf("Unexpected error creating new TrnType from string \"CREDIT\"\n")
|
||||||
}
|
}
|
||||||
@ -121,7 +121,7 @@ func TestTrnType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHER", &e)
|
marshalHelper(t, "OTHER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewTrnType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewTrnType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new TrnType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new TrnType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -149,7 +149,7 @@ func TestTrnType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImageType(t *testing.T) {
|
func TestImageType(t *testing.T) {
|
||||||
e, err := ofxgo.NewImageType("STATEMENT")
|
e, err := NewImageType("STATEMENT")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new ImageType from string \"STATEMENT\"\n")
|
t.Fatalf("Unexpected error creating new ImageType from string \"STATEMENT\"\n")
|
||||||
}
|
}
|
||||||
@ -166,7 +166,7 @@ func TestImageType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "TAX", &e)
|
marshalHelper(t, "TAX", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewImageType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewImageType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new ImageType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new ImageType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -194,7 +194,7 @@ func TestImageType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImageRefType(t *testing.T) {
|
func TestImageRefType(t *testing.T) {
|
||||||
e, err := ofxgo.NewImageRefType("OPAQUE")
|
e, err := NewImageRefType("OPAQUE")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new ImageRefType from string \"OPAQUE\"\n")
|
t.Fatalf("Unexpected error creating new ImageRefType from string \"OPAQUE\"\n")
|
||||||
}
|
}
|
||||||
@ -211,7 +211,7 @@ func TestImageRefType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "FORMURL", &e)
|
marshalHelper(t, "FORMURL", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewImageRefType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewImageRefType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new ImageRefType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new ImageRefType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -239,7 +239,7 @@ func TestImageRefType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckSup(t *testing.T) {
|
func TestCheckSup(t *testing.T) {
|
||||||
e, err := ofxgo.NewCheckSup("FRONTONLY")
|
e, err := NewCheckSup("FRONTONLY")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new CheckSup from string \"FRONTONLY\"\n")
|
t.Fatalf("Unexpected error creating new CheckSup from string \"FRONTONLY\"\n")
|
||||||
}
|
}
|
||||||
@ -256,7 +256,7 @@ func TestCheckSup(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "FRONTANDBACK", &e)
|
marshalHelper(t, "FRONTANDBACK", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewCheckSup("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewCheckSup("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new CheckSup from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new CheckSup from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -284,7 +284,7 @@ func TestCheckSup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCorrectAction(t *testing.T) {
|
func TestCorrectAction(t *testing.T) {
|
||||||
e, err := ofxgo.NewCorrectAction("DELETE")
|
e, err := NewCorrectAction("DELETE")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new CorrectAction from string \"DELETE\"\n")
|
t.Fatalf("Unexpected error creating new CorrectAction from string \"DELETE\"\n")
|
||||||
}
|
}
|
||||||
@ -301,7 +301,7 @@ func TestCorrectAction(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "REPLACE", &e)
|
marshalHelper(t, "REPLACE", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewCorrectAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewCorrectAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new CorrectAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new CorrectAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -329,7 +329,7 @@ func TestCorrectAction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBalType(t *testing.T) {
|
func TestBalType(t *testing.T) {
|
||||||
e, err := ofxgo.NewBalType("DOLLAR")
|
e, err := NewBalType("DOLLAR")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new BalType from string \"DOLLAR\"\n")
|
t.Fatalf("Unexpected error creating new BalType from string \"DOLLAR\"\n")
|
||||||
}
|
}
|
||||||
@ -346,7 +346,7 @@ func TestBalType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "NUMBER", &e)
|
marshalHelper(t, "NUMBER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewBalType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewBalType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new BalType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new BalType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -374,7 +374,7 @@ func TestBalType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInv401kSource(t *testing.T) {
|
func TestInv401kSource(t *testing.T) {
|
||||||
e, err := ofxgo.NewInv401kSource("PRETAX")
|
e, err := NewInv401kSource("PRETAX")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new Inv401kSource from string \"PRETAX\"\n")
|
t.Fatalf("Unexpected error creating new Inv401kSource from string \"PRETAX\"\n")
|
||||||
}
|
}
|
||||||
@ -391,7 +391,7 @@ func TestInv401kSource(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHERNONVEST", &e)
|
marshalHelper(t, "OTHERNONVEST", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewInv401kSource("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewInv401kSource("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new Inv401kSource from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new Inv401kSource from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -419,7 +419,7 @@ func TestInv401kSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSubAcctType(t *testing.T) {
|
func TestSubAcctType(t *testing.T) {
|
||||||
e, err := ofxgo.NewSubAcctType("CASH")
|
e, err := NewSubAcctType("CASH")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new SubAcctType from string \"CASH\"\n")
|
t.Fatalf("Unexpected error creating new SubAcctType from string \"CASH\"\n")
|
||||||
}
|
}
|
||||||
@ -436,7 +436,7 @@ func TestSubAcctType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHER", &e)
|
marshalHelper(t, "OTHER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewSubAcctType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewSubAcctType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new SubAcctType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new SubAcctType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -464,7 +464,7 @@ func TestSubAcctType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBuyType(t *testing.T) {
|
func TestBuyType(t *testing.T) {
|
||||||
e, err := ofxgo.NewBuyType("BUY")
|
e, err := NewBuyType("BUY")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new BuyType from string \"BUY\"\n")
|
t.Fatalf("Unexpected error creating new BuyType from string \"BUY\"\n")
|
||||||
}
|
}
|
||||||
@ -481,7 +481,7 @@ func TestBuyType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "BUYTOCOVER", &e)
|
marshalHelper(t, "BUYTOCOVER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewBuyType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewBuyType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new BuyType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new BuyType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -509,7 +509,7 @@ func TestBuyType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOptAction(t *testing.T) {
|
func TestOptAction(t *testing.T) {
|
||||||
e, err := ofxgo.NewOptAction("EXERCISE")
|
e, err := NewOptAction("EXERCISE")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new OptAction from string \"EXERCISE\"\n")
|
t.Fatalf("Unexpected error creating new OptAction from string \"EXERCISE\"\n")
|
||||||
}
|
}
|
||||||
@ -526,7 +526,7 @@ func TestOptAction(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "EXPIRE", &e)
|
marshalHelper(t, "EXPIRE", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewOptAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewOptAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new OptAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new OptAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -554,7 +554,7 @@ func TestOptAction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTferAction(t *testing.T) {
|
func TestTferAction(t *testing.T) {
|
||||||
e, err := ofxgo.NewTferAction("IN")
|
e, err := NewTferAction("IN")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new TferAction from string \"IN\"\n")
|
t.Fatalf("Unexpected error creating new TferAction from string \"IN\"\n")
|
||||||
}
|
}
|
||||||
@ -571,7 +571,7 @@ func TestTferAction(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OUT", &e)
|
marshalHelper(t, "OUT", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewTferAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewTferAction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new TferAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new TferAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -599,7 +599,7 @@ func TestTferAction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPosType(t *testing.T) {
|
func TestPosType(t *testing.T) {
|
||||||
e, err := ofxgo.NewPosType("LONG")
|
e, err := NewPosType("LONG")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new PosType from string \"LONG\"\n")
|
t.Fatalf("Unexpected error creating new PosType from string \"LONG\"\n")
|
||||||
}
|
}
|
||||||
@ -616,7 +616,7 @@ func TestPosType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "SHORT", &e)
|
marshalHelper(t, "SHORT", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewPosType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewPosType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new PosType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new PosType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -644,7 +644,7 @@ func TestPosType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSecured(t *testing.T) {
|
func TestSecured(t *testing.T) {
|
||||||
e, err := ofxgo.NewSecured("NAKED")
|
e, err := NewSecured("NAKED")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new Secured from string \"NAKED\"\n")
|
t.Fatalf("Unexpected error creating new Secured from string \"NAKED\"\n")
|
||||||
}
|
}
|
||||||
@ -661,7 +661,7 @@ func TestSecured(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "COVERED", &e)
|
marshalHelper(t, "COVERED", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewSecured("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewSecured("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new Secured from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new Secured from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -689,7 +689,7 @@ func TestSecured(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDuration(t *testing.T) {
|
func TestDuration(t *testing.T) {
|
||||||
e, err := ofxgo.NewDuration("DAY")
|
e, err := NewDuration("DAY")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new Duration from string \"DAY\"\n")
|
t.Fatalf("Unexpected error creating new Duration from string \"DAY\"\n")
|
||||||
}
|
}
|
||||||
@ -706,7 +706,7 @@ func TestDuration(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "IMMEDIATE", &e)
|
marshalHelper(t, "IMMEDIATE", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewDuration("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewDuration("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new Duration from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new Duration from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -734,7 +734,7 @@ func TestDuration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRestriction(t *testing.T) {
|
func TestRestriction(t *testing.T) {
|
||||||
e, err := ofxgo.NewRestriction("ALLORNONE")
|
e, err := NewRestriction("ALLORNONE")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new Restriction from string \"ALLORNONE\"\n")
|
t.Fatalf("Unexpected error creating new Restriction from string \"ALLORNONE\"\n")
|
||||||
}
|
}
|
||||||
@ -751,7 +751,7 @@ func TestRestriction(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "NONE", &e)
|
marshalHelper(t, "NONE", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewRestriction("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewRestriction("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new Restriction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new Restriction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -779,7 +779,7 @@ func TestRestriction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUnitType(t *testing.T) {
|
func TestUnitType(t *testing.T) {
|
||||||
e, err := ofxgo.NewUnitType("SHARES")
|
e, err := NewUnitType("SHARES")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new UnitType from string \"SHARES\"\n")
|
t.Fatalf("Unexpected error creating new UnitType from string \"SHARES\"\n")
|
||||||
}
|
}
|
||||||
@ -796,7 +796,7 @@ func TestUnitType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "CURRENCY", &e)
|
marshalHelper(t, "CURRENCY", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewUnitType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewUnitType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new UnitType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new UnitType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -824,7 +824,7 @@ func TestUnitType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOptBuyType(t *testing.T) {
|
func TestOptBuyType(t *testing.T) {
|
||||||
e, err := ofxgo.NewOptBuyType("BUYTOOPEN")
|
e, err := NewOptBuyType("BUYTOOPEN")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new OptBuyType from string \"BUYTOOPEN\"\n")
|
t.Fatalf("Unexpected error creating new OptBuyType from string \"BUYTOOPEN\"\n")
|
||||||
}
|
}
|
||||||
@ -841,7 +841,7 @@ func TestOptBuyType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "BUYTOCLOSE", &e)
|
marshalHelper(t, "BUYTOCLOSE", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewOptBuyType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewOptBuyType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new OptBuyType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new OptBuyType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -869,7 +869,7 @@ func TestOptBuyType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSellType(t *testing.T) {
|
func TestSellType(t *testing.T) {
|
||||||
e, err := ofxgo.NewSellType("SELL")
|
e, err := NewSellType("SELL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new SellType from string \"SELL\"\n")
|
t.Fatalf("Unexpected error creating new SellType from string \"SELL\"\n")
|
||||||
}
|
}
|
||||||
@ -886,7 +886,7 @@ func TestSellType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "SELLSHORT", &e)
|
marshalHelper(t, "SELLSHORT", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewSellType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewSellType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new SellType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new SellType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -914,7 +914,7 @@ func TestSellType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoanPmtFreq(t *testing.T) {
|
func TestLoanPmtFreq(t *testing.T) {
|
||||||
e, err := ofxgo.NewLoanPmtFreq("WEEKLY")
|
e, err := NewLoanPmtFreq("WEEKLY")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new LoanPmtFreq from string \"WEEKLY\"\n")
|
t.Fatalf("Unexpected error creating new LoanPmtFreq from string \"WEEKLY\"\n")
|
||||||
}
|
}
|
||||||
@ -931,7 +931,7 @@ func TestLoanPmtFreq(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHER", &e)
|
marshalHelper(t, "OTHER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewLoanPmtFreq("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewLoanPmtFreq("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new LoanPmtFreq from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new LoanPmtFreq from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -959,7 +959,7 @@ func TestLoanPmtFreq(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIncomeType(t *testing.T) {
|
func TestIncomeType(t *testing.T) {
|
||||||
e, err := ofxgo.NewIncomeType("CGLONG")
|
e, err := NewIncomeType("CGLONG")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new IncomeType from string \"CGLONG\"\n")
|
t.Fatalf("Unexpected error creating new IncomeType from string \"CGLONG\"\n")
|
||||||
}
|
}
|
||||||
@ -976,7 +976,7 @@ func TestIncomeType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "MISC", &e)
|
marshalHelper(t, "MISC", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewIncomeType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewIncomeType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new IncomeType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new IncomeType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1004,7 +1004,7 @@ func TestIncomeType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSellReason(t *testing.T) {
|
func TestSellReason(t *testing.T) {
|
||||||
e, err := ofxgo.NewSellReason("CALL")
|
e, err := NewSellReason("CALL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new SellReason from string \"CALL\"\n")
|
t.Fatalf("Unexpected error creating new SellReason from string \"CALL\"\n")
|
||||||
}
|
}
|
||||||
@ -1021,7 +1021,7 @@ func TestSellReason(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "MATURITY", &e)
|
marshalHelper(t, "MATURITY", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewSellReason("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewSellReason("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new SellReason from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new SellReason from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1049,7 +1049,7 @@ func TestSellReason(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOptSellType(t *testing.T) {
|
func TestOptSellType(t *testing.T) {
|
||||||
e, err := ofxgo.NewOptSellType("SELLTOCLOSE")
|
e, err := NewOptSellType("SELLTOCLOSE")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new OptSellType from string \"SELLTOCLOSE\"\n")
|
t.Fatalf("Unexpected error creating new OptSellType from string \"SELLTOCLOSE\"\n")
|
||||||
}
|
}
|
||||||
@ -1066,7 +1066,7 @@ func TestOptSellType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "SELLTOOPEN", &e)
|
marshalHelper(t, "SELLTOOPEN", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewOptSellType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewOptSellType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new OptSellType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new OptSellType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1094,7 +1094,7 @@ func TestOptSellType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRelType(t *testing.T) {
|
func TestRelType(t *testing.T) {
|
||||||
e, err := ofxgo.NewRelType("SPREAD")
|
e, err := NewRelType("SPREAD")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new RelType from string \"SPREAD\"\n")
|
t.Fatalf("Unexpected error creating new RelType from string \"SPREAD\"\n")
|
||||||
}
|
}
|
||||||
@ -1111,7 +1111,7 @@ func TestRelType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHER", &e)
|
marshalHelper(t, "OTHER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewRelType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewRelType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new RelType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new RelType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1139,7 +1139,7 @@ func TestRelType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCharType(t *testing.T) {
|
func TestCharType(t *testing.T) {
|
||||||
e, err := ofxgo.NewCharType("ALPHAONLY")
|
e, err := NewCharType("ALPHAONLY")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new CharType from string \"ALPHAONLY\"\n")
|
t.Fatalf("Unexpected error creating new CharType from string \"ALPHAONLY\"\n")
|
||||||
}
|
}
|
||||||
@ -1156,7 +1156,7 @@ func TestCharType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "ALPHAANDNUMERIC", &e)
|
marshalHelper(t, "ALPHAANDNUMERIC", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewCharType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewCharType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new CharType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new CharType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1184,7 +1184,7 @@ func TestCharType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSyncMode(t *testing.T) {
|
func TestSyncMode(t *testing.T) {
|
||||||
e, err := ofxgo.NewSyncMode("FULL")
|
e, err := NewSyncMode("FULL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new SyncMode from string \"FULL\"\n")
|
t.Fatalf("Unexpected error creating new SyncMode from string \"FULL\"\n")
|
||||||
}
|
}
|
||||||
@ -1201,7 +1201,7 @@ func TestSyncMode(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "LITE", &e)
|
marshalHelper(t, "LITE", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewSyncMode("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewSyncMode("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new SyncMode from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new SyncMode from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1229,7 +1229,7 @@ func TestSyncMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOfxSec(t *testing.T) {
|
func TestOfxSec(t *testing.T) {
|
||||||
e, err := ofxgo.NewOfxSec("NONE")
|
e, err := NewOfxSec("NONE")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new OfxSec from string \"NONE\"\n")
|
t.Fatalf("Unexpected error creating new OfxSec from string \"NONE\"\n")
|
||||||
}
|
}
|
||||||
@ -1246,7 +1246,7 @@ func TestOfxSec(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "TYPE 1", &e)
|
marshalHelper(t, "TYPE 1", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewOfxSec("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewOfxSec("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new OfxSec from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new OfxSec from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1274,7 +1274,7 @@ func TestOfxSec(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDebtType(t *testing.T) {
|
func TestDebtType(t *testing.T) {
|
||||||
e, err := ofxgo.NewDebtType("COUPON")
|
e, err := NewDebtType("COUPON")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new DebtType from string \"COUPON\"\n")
|
t.Fatalf("Unexpected error creating new DebtType from string \"COUPON\"\n")
|
||||||
}
|
}
|
||||||
@ -1291,7 +1291,7 @@ func TestDebtType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "ZERO", &e)
|
marshalHelper(t, "ZERO", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewDebtType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewDebtType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new DebtType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new DebtType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1319,7 +1319,7 @@ func TestDebtType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDebtClass(t *testing.T) {
|
func TestDebtClass(t *testing.T) {
|
||||||
e, err := ofxgo.NewDebtClass("TREASURY")
|
e, err := NewDebtClass("TREASURY")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new DebtClass from string \"TREASURY\"\n")
|
t.Fatalf("Unexpected error creating new DebtClass from string \"TREASURY\"\n")
|
||||||
}
|
}
|
||||||
@ -1336,7 +1336,7 @@ func TestDebtClass(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHER", &e)
|
marshalHelper(t, "OTHER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewDebtClass("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewDebtClass("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new DebtClass from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new DebtClass from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1364,7 +1364,7 @@ func TestDebtClass(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCouponFreq(t *testing.T) {
|
func TestCouponFreq(t *testing.T) {
|
||||||
e, err := ofxgo.NewCouponFreq("MONTHLY")
|
e, err := NewCouponFreq("MONTHLY")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new CouponFreq from string \"MONTHLY\"\n")
|
t.Fatalf("Unexpected error creating new CouponFreq from string \"MONTHLY\"\n")
|
||||||
}
|
}
|
||||||
@ -1381,7 +1381,7 @@ func TestCouponFreq(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHER", &e)
|
marshalHelper(t, "OTHER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewCouponFreq("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewCouponFreq("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new CouponFreq from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new CouponFreq from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1409,7 +1409,7 @@ func TestCouponFreq(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCallType(t *testing.T) {
|
func TestCallType(t *testing.T) {
|
||||||
e, err := ofxgo.NewCallType("CALL")
|
e, err := NewCallType("CALL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new CallType from string \"CALL\"\n")
|
t.Fatalf("Unexpected error creating new CallType from string \"CALL\"\n")
|
||||||
}
|
}
|
||||||
@ -1426,7 +1426,7 @@ func TestCallType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "MATURITY", &e)
|
marshalHelper(t, "MATURITY", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewCallType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewCallType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new CallType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new CallType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1454,7 +1454,7 @@ func TestCallType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAssetClass(t *testing.T) {
|
func TestAssetClass(t *testing.T) {
|
||||||
e, err := ofxgo.NewAssetClass("DOMESTICBOND")
|
e, err := NewAssetClass("DOMESTICBOND")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new AssetClass from string \"DOMESTICBOND\"\n")
|
t.Fatalf("Unexpected error creating new AssetClass from string \"DOMESTICBOND\"\n")
|
||||||
}
|
}
|
||||||
@ -1471,7 +1471,7 @@ func TestAssetClass(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHER", &e)
|
marshalHelper(t, "OTHER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewAssetClass("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewAssetClass("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new AssetClass from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new AssetClass from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1499,7 +1499,7 @@ func TestAssetClass(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMfType(t *testing.T) {
|
func TestMfType(t *testing.T) {
|
||||||
e, err := ofxgo.NewMfType("OPENEND")
|
e, err := NewMfType("OPENEND")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new MfType from string \"OPENEND\"\n")
|
t.Fatalf("Unexpected error creating new MfType from string \"OPENEND\"\n")
|
||||||
}
|
}
|
||||||
@ -1516,7 +1516,7 @@ func TestMfType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHER", &e)
|
marshalHelper(t, "OTHER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewMfType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewMfType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new MfType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new MfType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1544,7 +1544,7 @@ func TestMfType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOptType(t *testing.T) {
|
func TestOptType(t *testing.T) {
|
||||||
e, err := ofxgo.NewOptType("PUT")
|
e, err := NewOptType("PUT")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new OptType from string \"PUT\"\n")
|
t.Fatalf("Unexpected error creating new OptType from string \"PUT\"\n")
|
||||||
}
|
}
|
||||||
@ -1561,7 +1561,7 @@ func TestOptType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "CALL", &e)
|
marshalHelper(t, "CALL", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewOptType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewOptType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new OptType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new OptType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1589,7 +1589,7 @@ func TestOptType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStockType(t *testing.T) {
|
func TestStockType(t *testing.T) {
|
||||||
e, err := ofxgo.NewStockType("COMMON")
|
e, err := NewStockType("COMMON")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new StockType from string \"COMMON\"\n")
|
t.Fatalf("Unexpected error creating new StockType from string \"COMMON\"\n")
|
||||||
}
|
}
|
||||||
@ -1606,7 +1606,7 @@ func TestStockType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHER", &e)
|
marshalHelper(t, "OTHER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewStockType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewStockType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new StockType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new StockType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1634,7 +1634,7 @@ func TestStockType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHolderType(t *testing.T) {
|
func TestHolderType(t *testing.T) {
|
||||||
e, err := ofxgo.NewHolderType("INDIVIDUAL")
|
e, err := NewHolderType("INDIVIDUAL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new HolderType from string \"INDIVIDUAL\"\n")
|
t.Fatalf("Unexpected error creating new HolderType from string \"INDIVIDUAL\"\n")
|
||||||
}
|
}
|
||||||
@ -1651,7 +1651,7 @@ func TestHolderType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHER", &e)
|
marshalHelper(t, "OTHER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewHolderType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewHolderType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new HolderType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new HolderType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1679,7 +1679,7 @@ func TestHolderType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAcctClassification(t *testing.T) {
|
func TestAcctClassification(t *testing.T) {
|
||||||
e, err := ofxgo.NewAcctClassification("PERSONAL")
|
e, err := NewAcctClassification("PERSONAL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new AcctClassification from string \"PERSONAL\"\n")
|
t.Fatalf("Unexpected error creating new AcctClassification from string \"PERSONAL\"\n")
|
||||||
}
|
}
|
||||||
@ -1696,7 +1696,7 @@ func TestAcctClassification(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "OTHER", &e)
|
marshalHelper(t, "OTHER", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewAcctClassification("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewAcctClassification("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new AcctClassification from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new AcctClassification from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1724,7 +1724,7 @@ func TestAcctClassification(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSvcStatus(t *testing.T) {
|
func TestSvcStatus(t *testing.T) {
|
||||||
e, err := ofxgo.NewSvcStatus("AVAIL")
|
e, err := NewSvcStatus("AVAIL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new SvcStatus from string \"AVAIL\"\n")
|
t.Fatalf("Unexpected error creating new SvcStatus from string \"AVAIL\"\n")
|
||||||
}
|
}
|
||||||
@ -1741,7 +1741,7 @@ func TestSvcStatus(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "ACTIVE", &e)
|
marshalHelper(t, "ACTIVE", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewSvcStatus("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewSvcStatus("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new SvcStatus from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new SvcStatus from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
@ -1769,7 +1769,7 @@ func TestSvcStatus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUsProductType(t *testing.T) {
|
func TestUsProductType(t *testing.T) {
|
||||||
e, err := ofxgo.NewUsProductType("401K")
|
e, err := NewUsProductType("401K")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating new UsProductType from string \"401K\"\n")
|
t.Fatalf("Unexpected error creating new UsProductType from string \"401K\"\n")
|
||||||
}
|
}
|
||||||
@ -1786,7 +1786,7 @@ func TestUsProductType(t *testing.T) {
|
|||||||
|
|
||||||
marshalHelper(t, "UGMA", &e)
|
marshalHelper(t, "UGMA", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.NewUsProductType("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := NewUsProductType("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error creating new UsProductType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
t.Fatalf("Expected error creating new UsProductType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n")
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package ofxgo_test
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -41,31 +40,31 @@ func TestMarshalCCStatementRequest(t *testing.T) {
|
|||||||
</CREDITCARDMSGSRQV1>
|
</CREDITCARDMSGSRQV1>
|
||||||
</OFX>`
|
</OFX>`
|
||||||
|
|
||||||
var client = ofxgo.Client{
|
var client = BasicClient{
|
||||||
AppID: "OFXGO",
|
AppID: "OFXGO",
|
||||||
AppVer: "0001",
|
AppVer: "0001",
|
||||||
SpecVersion: ofxgo.OfxVersion203,
|
SpecVersion: OfxVersion203,
|
||||||
}
|
}
|
||||||
|
|
||||||
var request ofxgo.Request
|
var request Request
|
||||||
request.Signon.UserID = "myusername"
|
request.Signon.UserID = "myusername"
|
||||||
request.Signon.UserPass = "Pa$$word"
|
request.Signon.UserPass = "Pa$$word"
|
||||||
request.Signon.Org = "BNK"
|
request.Signon.Org = "BNK"
|
||||||
request.Signon.Fid = "1987"
|
request.Signon.Fid = "1987"
|
||||||
|
|
||||||
statementRequest := ofxgo.CCStatementRequest{
|
statementRequest := CCStatementRequest{
|
||||||
TrnUID: "913846",
|
TrnUID: "913846",
|
||||||
CCAcctFrom: ofxgo.CCAcct{
|
CCAcctFrom: CCAcct{
|
||||||
AcctID: "XXXXXXXXXXXX1234",
|
AcctID: "XXXXXXXXXXXX1234",
|
||||||
},
|
},
|
||||||
DtStart: ofxgo.NewDateGMT(2017, 1, 1, 0, 0, 0, 0),
|
DtStart: NewDateGMT(2017, 1, 1, 0, 0, 0, 0),
|
||||||
Include: true,
|
Include: true,
|
||||||
}
|
}
|
||||||
request.CreditCard = append(request.CreditCard, &statementRequest)
|
request.CreditCard = append(request.CreditCard, &statementRequest)
|
||||||
|
|
||||||
request.SetClientFields(&client)
|
request.SetClientFields(&client)
|
||||||
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
||||||
request.Signon.DtClient = *ofxgo.NewDateGMT(2017, 3, 31, 15, 38, 48, 0)
|
request.Signon.DtClient = *NewDateGMT(2017, 3, 31, 15, 38, 48, 0)
|
||||||
|
|
||||||
marshalCheckRequest(t, &request, expectedString)
|
marshalCheckRequest(t, &request, expectedString)
|
||||||
}
|
}
|
||||||
@ -82,45 +81,45 @@ OLDFILEUID:NONE
|
|||||||
NEWFILEUID:NONE
|
NEWFILEUID:NONE
|
||||||
|
|
||||||
<OFX><SIGNONMSGSRSV1><SONRS><STATUS><CODE>0<SEVERITY>INFO<MESSAGE>SUCCESS</STATUS><DTSERVER>20170331154648.331[-4:EDT]<LANGUAGE>ENG<FI><ORG>01<FID>81729</FI></SONRS></SIGNONMSGSRSV1><CREDITCARDMSGSRSV1><CCSTMTTRNRS><TRNUID>59e850ad-7448-b4ce-4b71-29057763b306<STATUS><CODE>0<SEVERITY>INFO</STATUS><CCSTMTRS><CURDEF>USD<CCACCTFROM><ACCTID>9283744488463775</CCACCTFROM><BANKTRANLIST><DTSTART>20161201154648.688[-5:EST]<DTEND>20170331154648.688[-4:EDT]<STMTTRN><TRNTYPE>DEBIT<DTPOSTED>20170209120000[0:GMT]<TRNAMT>-7.96<FITID>2017020924435657040207171600195<NAME>SLICE OF NY</STMTTRN><STMTTRN><TRNTYPE>CREDIT<DTPOSTED>20161228120000[0:GMT]<TRNAMT>3830.46<FITID>2016122823633637200000258482730<NAME>Payment Thank You Electro</STMTTRN><STMTTRN><TRNTYPE>DEBIT<DTPOSTED>20170327120000[0:GMT]<TRNAMT>-17.7<FITID>2017032724445727085300442885680<NAME>KROGER FUEL #9999</STMTTRN></BANKTRANLIST><LEDGERBAL><BALAMT>-9334<DTASOF>20170331080000.000[-4:EDT]</LEDGERBAL><AVAILBAL><BALAMT>7630.17<DTASOF>20170331080000.000[-4:EDT]</AVAILBAL></CCSTMTRS></CCSTMTTRNRS></CREDITCARDMSGSRSV1></OFX>`)
|
<OFX><SIGNONMSGSRSV1><SONRS><STATUS><CODE>0<SEVERITY>INFO<MESSAGE>SUCCESS</STATUS><DTSERVER>20170331154648.331[-4:EDT]<LANGUAGE>ENG<FI><ORG>01<FID>81729</FI></SONRS></SIGNONMSGSRSV1><CREDITCARDMSGSRSV1><CCSTMTTRNRS><TRNUID>59e850ad-7448-b4ce-4b71-29057763b306<STATUS><CODE>0<SEVERITY>INFO</STATUS><CCSTMTRS><CURDEF>USD<CCACCTFROM><ACCTID>9283744488463775</CCACCTFROM><BANKTRANLIST><DTSTART>20161201154648.688[-5:EST]<DTEND>20170331154648.688[-4:EDT]<STMTTRN><TRNTYPE>DEBIT<DTPOSTED>20170209120000[0:GMT]<TRNAMT>-7.96<FITID>2017020924435657040207171600195<NAME>SLICE OF NY</STMTTRN><STMTTRN><TRNTYPE>CREDIT<DTPOSTED>20161228120000[0:GMT]<TRNAMT>3830.46<FITID>2016122823633637200000258482730<NAME>Payment Thank You Electro</STMTTRN><STMTTRN><TRNTYPE>DEBIT<DTPOSTED>20170327120000[0:GMT]<TRNAMT>-17.7<FITID>2017032724445727085300442885680<NAME>KROGER FUEL #9999</STMTTRN></BANKTRANLIST><LEDGERBAL><BALAMT>-9334<DTASOF>20170331080000.000[-4:EDT]</LEDGERBAL><AVAILBAL><BALAMT>7630.17<DTASOF>20170331080000.000[-4:EDT]</AVAILBAL></CCSTMTRS></CCSTMTTRNRS></CREDITCARDMSGSRSV1></OFX>`)
|
||||||
var expected ofxgo.Response
|
var expected Response
|
||||||
EDT := time.FixedZone("EDT", -4*60*60)
|
EDT := time.FixedZone("EDT", -4*60*60)
|
||||||
EST := time.FixedZone("EST", -5*60*60)
|
EST := time.FixedZone("EST", -5*60*60)
|
||||||
|
|
||||||
expected.Version = ofxgo.OfxVersion102
|
expected.Version = OfxVersion102
|
||||||
expected.Signon.Status.Code = 0
|
expected.Signon.Status.Code = 0
|
||||||
expected.Signon.Status.Severity = "INFO"
|
expected.Signon.Status.Severity = "INFO"
|
||||||
expected.Signon.Status.Message = "SUCCESS"
|
expected.Signon.Status.Message = "SUCCESS"
|
||||||
expected.Signon.DtServer = *ofxgo.NewDate(2017, 3, 31, 15, 46, 48, 331000000, EDT)
|
expected.Signon.DtServer = *NewDate(2017, 3, 31, 15, 46, 48, 331000000, EDT)
|
||||||
expected.Signon.Language = "ENG"
|
expected.Signon.Language = "ENG"
|
||||||
expected.Signon.Org = "01"
|
expected.Signon.Org = "01"
|
||||||
expected.Signon.Fid = "81729"
|
expected.Signon.Fid = "81729"
|
||||||
|
|
||||||
var trnamt1, trnamt2, trnamt3 ofxgo.Amount
|
var trnamt1, trnamt2, trnamt3 Amount
|
||||||
trnamt1.SetFrac64(-796, 100)
|
trnamt1.SetFrac64(-796, 100)
|
||||||
trnamt2.SetFrac64(383046, 100)
|
trnamt2.SetFrac64(383046, 100)
|
||||||
trnamt3.SetFrac64(-1770, 100)
|
trnamt3.SetFrac64(-1770, 100)
|
||||||
|
|
||||||
banktranlist := ofxgo.TransactionList{
|
banktranlist := TransactionList{
|
||||||
DtStart: *ofxgo.NewDate(2016, 12, 1, 15, 46, 48, 688000000, EST),
|
DtStart: *NewDate(2016, 12, 1, 15, 46, 48, 688000000, EST),
|
||||||
DtEnd: *ofxgo.NewDate(2017, 3, 31, 15, 46, 48, 688000000, EDT),
|
DtEnd: *NewDate(2017, 3, 31, 15, 46, 48, 688000000, EDT),
|
||||||
Transactions: []ofxgo.Transaction{
|
Transactions: []Transaction{
|
||||||
{
|
{
|
||||||
TrnType: ofxgo.TrnTypeDebit,
|
TrnType: TrnTypeDebit,
|
||||||
DtPosted: *ofxgo.NewDateGMT(2017, 2, 9, 12, 0, 0, 0),
|
DtPosted: *NewDateGMT(2017, 2, 9, 12, 0, 0, 0),
|
||||||
TrnAmt: trnamt1,
|
TrnAmt: trnamt1,
|
||||||
FiTID: "2017020924435657040207171600195",
|
FiTID: "2017020924435657040207171600195",
|
||||||
Name: "SLICE OF NY",
|
Name: "SLICE OF NY",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TrnType: ofxgo.TrnTypeCredit,
|
TrnType: TrnTypeCredit,
|
||||||
DtPosted: *ofxgo.NewDateGMT(2016, 12, 28, 12, 0, 0, 0),
|
DtPosted: *NewDateGMT(2016, 12, 28, 12, 0, 0, 0),
|
||||||
TrnAmt: trnamt2,
|
TrnAmt: trnamt2,
|
||||||
FiTID: "2016122823633637200000258482730",
|
FiTID: "2016122823633637200000258482730",
|
||||||
Name: "Payment Thank You Electro",
|
Name: "Payment Thank You Electro",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
TrnType: ofxgo.TrnTypeDebit,
|
TrnType: TrnTypeDebit,
|
||||||
DtPosted: *ofxgo.NewDateGMT(2017, 3, 27, 12, 0, 0, 0),
|
DtPosted: *NewDateGMT(2017, 3, 27, 12, 0, 0, 0),
|
||||||
TrnAmt: trnamt3,
|
TrnAmt: trnamt3,
|
||||||
FiTID: "2017032724445727085300442885680",
|
FiTID: "2017032724445727085300442885680",
|
||||||
Name: "KROGER FUEL #9999",
|
Name: "KROGER FUEL #9999",
|
||||||
@ -128,37 +127,38 @@ NEWFILEUID:NONE
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var balamt, availbalamt ofxgo.Amount
|
var balamt, availbalamt Amount
|
||||||
balamt.SetFrac64(-933400, 100)
|
balamt.SetFrac64(-933400, 100)
|
||||||
availbalamt.SetFrac64(763017, 100)
|
availbalamt.SetFrac64(763017, 100)
|
||||||
|
|
||||||
usd, err := ofxgo.NewCurrSymbol("USD")
|
usd, err := NewCurrSymbol("USD")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error creating CurrSymbol for USD\n")
|
t.Fatalf("Unexpected error creating CurrSymbol for USD\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
statementResponse := ofxgo.CCStatementResponse{
|
statementResponse := CCStatementResponse{
|
||||||
TrnUID: "59e850ad-7448-b4ce-4b71-29057763b306",
|
TrnUID: "59e850ad-7448-b4ce-4b71-29057763b306",
|
||||||
Status: ofxgo.Status{
|
Status: Status{
|
||||||
Code: 0,
|
Code: 0,
|
||||||
Severity: "INFO",
|
Severity: "INFO",
|
||||||
},
|
},
|
||||||
CurDef: *usd,
|
CurDef: *usd,
|
||||||
CCAcctFrom: ofxgo.CCAcct{
|
CCAcctFrom: CCAcct{
|
||||||
AcctID: "9283744488463775",
|
AcctID: "9283744488463775",
|
||||||
},
|
},
|
||||||
BankTranList: &banktranlist,
|
BankTranList: &banktranlist,
|
||||||
BalAmt: balamt,
|
BalAmt: balamt,
|
||||||
DtAsOf: *ofxgo.NewDate(2017, 3, 31, 8, 0, 0, 0, EDT),
|
DtAsOf: *NewDate(2017, 3, 31, 8, 0, 0, 0, EDT),
|
||||||
AvailBalAmt: &availbalamt,
|
AvailBalAmt: &availbalamt,
|
||||||
AvailDtAsOf: ofxgo.NewDate(2017, 3, 31, 8, 0, 0, 0, EDT),
|
AvailDtAsOf: NewDate(2017, 3, 31, 8, 0, 0, 0, EDT),
|
||||||
}
|
}
|
||||||
expected.CreditCard = append(expected.CreditCard, &statementResponse)
|
expected.CreditCard = append(expected.CreditCard, &statementResponse)
|
||||||
|
|
||||||
response, err := ofxgo.ParseResponse(responseReader)
|
response, err := ParseResponse(responseReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkResponsesEqual(t, &expected, response)
|
checkResponsesEqual(t, &expected, response)
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
}
|
}
|
||||||
|
109
discovercard_client.go
Normal file
109
discovercard_client.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package ofxgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DiscoverCardClient provides a Client implementation which handles
|
||||||
|
// DiscoverCard's broken HTTP header behavior. DiscoverCardClient uses default,
|
||||||
|
// non-zero settings, if its fields are not initialized.
|
||||||
|
type DiscoverCardClient struct {
|
||||||
|
*BasicClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDiscoverCardClient returns a Client interface configured to handle
|
||||||
|
// Discover Card's brand of idiosyncrasy
|
||||||
|
func NewDiscoverCardClient(bc *BasicClient) Client {
|
||||||
|
return &DiscoverCardClient{bc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverCardHTTPPost(URL string, r io.Reader) (*http.Response, error) {
|
||||||
|
// Either convert or copy to a bytes.Buffer to be able to determine the
|
||||||
|
// request length for the Content-Length header
|
||||||
|
buf, ok := r.(*bytes.Buffer)
|
||||||
|
if !ok {
|
||||||
|
buf = &bytes.Buffer{}
|
||||||
|
_, err := io.Copy(buf, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := url.Parse(URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := url.Path
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover requires only these headers and in this exact order, or it
|
||||||
|
// returns HTTP 403
|
||||||
|
headers := fmt.Sprintf("POST %s HTTP/1.1\r\n"+
|
||||||
|
"Content-Type: application/x-ofx\r\n"+
|
||||||
|
"Host: %s\r\n"+
|
||||||
|
"Content-Length: %d\r\n"+
|
||||||
|
"Connection: Keep-Alive\r\n"+
|
||||||
|
"\r\n", path, url.Hostname(), buf.Len())
|
||||||
|
|
||||||
|
host := url.Host
|
||||||
|
if url.Port() == "" {
|
||||||
|
host += ":443"
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUGBUG: cannot do defer conn.Close() until body is read,
|
||||||
|
// we are "leaking" a socket here, but it will be finalized
|
||||||
|
conn, err := tls.Dial("tcp", host, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(conn, headers)
|
||||||
|
_, err = io.Copy(conn, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if !strings.HasPrefix(URL, "https://") {
|
||||||
|
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := discoverCardHTTPPost(URL, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return nil, errors.New("OFXQuery request status: " + response.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
return clientRequest(c, r)
|
||||||
|
}
|
27
doc.go
27
doc.go
@ -71,33 +71,32 @@ account and print the balance:
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var client ofxgo.Client // By not initializing them, we accept all default
|
var client Client // By not initializing them, we accept all default
|
||||||
// client values
|
// client values
|
||||||
var request ofxgo.Request
|
var request Request
|
||||||
|
|
||||||
// These are all specific to you and your financial institution
|
// These are all specific to you and your financial institution
|
||||||
request.URL = "https://ofx.example.com"
|
request.URL = "https://ofx.example.com"
|
||||||
request.Signon.UserID = ofxgo.String("john")
|
request.Signon.UserID = String("john")
|
||||||
request.Signon.UserPass = ofxgo.String("hunter2")
|
request.Signon.UserPass = String("hunter2")
|
||||||
request.Signon.Org = ofxgo.String("MyBank")
|
request.Signon.Org = String("MyBank")
|
||||||
request.Signon.Fid = ofxgo.String("0001")
|
request.Signon.Fid = String("0001")
|
||||||
|
|
||||||
uid, err := ofxgo.RandomUID()
|
uid, err := RandomUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error creating uid for transaction:", err)
|
fmt.Println("Error creating uid for transaction:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
statementRequest := ofxgo.StatementRequest{
|
statementRequest := StatementRequest{
|
||||||
TrnUID: *uid,
|
TrnUID: *uid,
|
||||||
BankAcctFrom: ofxgo.BankAcct{
|
BankAcctFrom: BankAcct{
|
||||||
BankID: ofxgo.String("123456789"),
|
BankID: String("123456789"),
|
||||||
AcctID: ofxgo.String("11111111111"),
|
AcctID: String("11111111111"),
|
||||||
AcctType: ofxgo.AcctTypeChecking,
|
AcctType: AcctTypeChecking,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,7 +116,7 @@ account and print the balance:
|
|||||||
|
|
||||||
if len(response.Bank) < 1 {
|
if len(response.Bank) < 1 {
|
||||||
fmt.Println("No banking messages received")
|
fmt.Println("No banking messages received")
|
||||||
} else if stmt, ok := response.Bank[0].(*ofxgo.StatementResponse); ok {
|
} else if stmt, ok := response.Bank[0].(*StatementResponse); ok {
|
||||||
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf)
|
fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,8 +65,9 @@ header = """package ofxgo
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/aclindsa/xml"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aclindsa/xml"
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -164,7 +165,7 @@ with open("constants.go", 'w') as f:
|
|||||||
constNames=constNames,
|
constNames=constNames,
|
||||||
upperValueString=upperValueString))
|
upperValueString=upperValueString))
|
||||||
|
|
||||||
test_header = """package ofxgo_test
|
test_header = """package ofxgo
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Do not edit this file by hand. It is auto-generated by calling `go generate`.
|
* Do not edit this file by hand. It is auto-generated by calling `go generate`.
|
||||||
@ -173,16 +174,16 @@ test_header = """package ofxgo_test
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aclindsa/xml"
|
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aclindsa/xml"
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
test_template = """
|
test_template = """
|
||||||
func Test{enum}(t *testing.T) {{
|
func Test{enum}(t *testing.T) {{
|
||||||
e, err := ofxgo.New{enum}("{firstValueUpper}")
|
e, err := New{enum}("{firstValueUpper}")
|
||||||
if err != nil {{
|
if err != nil {{
|
||||||
t.Fatalf("Unexpected error creating new {enum} from string \\\"{firstValueUpper}\\\"\\n")
|
t.Fatalf("Unexpected error creating new {enum} from string \\\"{firstValueUpper}\\\"\\n")
|
||||||
}}
|
}}
|
||||||
@ -199,7 +200,7 @@ func Test{enum}(t *testing.T) {{
|
|||||||
|
|
||||||
marshalHelper(t, "{lastValueUpper}", &e)
|
marshalHelper(t, "{lastValueUpper}", &e)
|
||||||
|
|
||||||
overwritten, err := ofxgo.New{enum}("THISWILLNEVERBEAVALIDENUMSTRING")
|
overwritten, err := New{enum}("THISWILLNEVERBEAVALIDENUMSTRING")
|
||||||
if err == nil {{
|
if err == nil {{
|
||||||
t.Fatalf("Expected error creating new {enum} from string \\\"THISWILLNEVERBEAVALIDENUMSTRING\\\"\\n")
|
t.Fatalf("Expected error creating new {enum} from string \\\"THISWILLNEVERBEAVALIDENUMSTRING\\\"\\n")
|
||||||
}}
|
}}
|
||||||
|
9
go.mod
Normal file
9
go.mod
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module github.com/aclindsa/ofxgo
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aclindsa/xml v0.0.0-20201125035057-bbd5c9ec99ac
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||||
|
golang.org/x/text v0.3.7
|
||||||
|
)
|
||||||
|
|
||||||
|
go 1.9
|
9
go.sum
Normal file
9
go.sum
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
github.com/aclindsa/xml v0.0.0-20201125035057-bbd5c9ec99ac h1:xCNSfPWpcx3Sdz/+aB/Re4L8oA6Y4kRRRuTh1CHCDEw=
|
||||||
|
github.com/aclindsa/xml v0.0.0-20201125035057-bbd5c9ec99ac/go.mod h1:GjqOUT8xlg5+T19lFv6yAGNrtMKkZ839Gt4e16mBXlY=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
222
invstmt.go
222
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
|
||||||
@ -113,7 +113,7 @@ type InvSell struct {
|
|||||||
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
|
||||||
@ -465,6 +465,7 @@ type InvBankTransaction struct {
|
|||||||
// security-related transactions themselves. It must be unmarshalled manually
|
// security-related transactions themselves. It must be unmarshalled manually
|
||||||
// due to the structure (don't know what kind of InvTransaction is coming next)
|
// due to the structure (don't know what kind of InvTransaction is coming next)
|
||||||
type InvTranList struct {
|
type InvTranList struct {
|
||||||
|
XMLName xml.Name `xml:"INVTRANLIST"`
|
||||||
DtStart Date
|
DtStart Date
|
||||||
DtEnd Date // This is the value that should be sent as <DTSTART> in the next InvStatementRequest to ensure that no transactions are missed
|
DtEnd Date // This is the value that should be sent as <DTSTART> in the next InvStatementRequest to ensure that no transactions are missed
|
||||||
InvTransactions []InvTransaction
|
InvTransactions []InvTransaction
|
||||||
@ -630,6 +631,119 @@ func (l *InvTranList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML handles marshalling an InvTranList element to an SGML/XML string
|
||||||
|
func (l *InvTranList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
invTranListElement := xml.StartElement{Name: xml.Name{Local: "INVTRANLIST"}}
|
||||||
|
if err := e.EncodeToken(invTranListElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err := e.EncodeElement(&l.DtStart, xml.StartElement{Name: xml.Name{Local: "DTSTART"}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = e.EncodeElement(&l.DtEnd, xml.StartElement{Name: xml.Name{Local: "DTEND"}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, t := range l.InvTransactions {
|
||||||
|
start := xml.StartElement{Name: xml.Name{Local: t.TransactionType()}}
|
||||||
|
switch tran := t.(type) {
|
||||||
|
case BuyDebt:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case BuyMF:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case BuyOpt:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case BuyOther:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case BuyStock:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case ClosureOpt:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case Income:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case InvExpense:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case JrnlFund:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case JrnlSec:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case MarginInterest:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case Reinvest:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case RetOfCap:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case SellDebt:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case SellMF:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case SellOpt:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case SellOther:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case SellStock:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case Split:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case Transfer:
|
||||||
|
if err := e.EncodeElement(&tran, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("Invalid INVTRANLIST child type: " + tran.TransactionType())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, tran := range l.BankTransactions {
|
||||||
|
err = e.EncodeElement(&tran, xml.StartElement{Name: xml.Name{Local: "INVBANKTRAN"}})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(invTranListElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// InvPosition contains generic position information included in each of the
|
// InvPosition contains generic position information included in each of the
|
||||||
// other *Position types
|
// other *Position types
|
||||||
type InvPosition struct {
|
type InvPosition struct {
|
||||||
@ -770,6 +884,45 @@ func (p *PositionList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML handles marshalling a PositionList to an XML string
|
||||||
|
func (p *PositionList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
invPosListElement := xml.StartElement{Name: xml.Name{Local: "INVPOSLIST"}}
|
||||||
|
if err := e.EncodeToken(invPosListElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, position := range *p {
|
||||||
|
start := xml.StartElement{Name: xml.Name{Local: position.PositionType()}}
|
||||||
|
switch pos := position.(type) {
|
||||||
|
case DebtPosition:
|
||||||
|
if err := e.EncodeElement(&pos, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case MFPosition:
|
||||||
|
if err := e.EncodeElement(&pos, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OptPosition:
|
||||||
|
if err := e.EncodeElement(&pos, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OtherPosition:
|
||||||
|
if err := e.EncodeElement(&pos, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case StockPosition:
|
||||||
|
if err := e.EncodeElement(&pos, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("Invalid INVPOSLIST child type: " + pos.PositionType())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(invPosListElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// InvBalance contains three (or optionally four) specified balances as well as
|
// InvBalance contains three (or optionally four) specified balances as well as
|
||||||
// a free-form list of generic balance information which may be provided by an
|
// a free-form list of generic balance information which may be provided by an
|
||||||
// FI.
|
// FI.
|
||||||
@ -1036,6 +1189,69 @@ func (o *OOList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML handles marshalling an OOList to an XML string
|
||||||
|
func (o *OOList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
ooListElement := xml.StartElement{Name: xml.Name{Local: "INVOOLIST"}}
|
||||||
|
if err := e.EncodeToken(ooListElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, openorder := range *o {
|
||||||
|
start := xml.StartElement{Name: xml.Name{Local: openorder.OrderType()}}
|
||||||
|
switch oo := openorder.(type) {
|
||||||
|
case OOBuyDebt:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOBuyMF:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOBuyOpt:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOBuyOther:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOBuyStock:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSellDebt:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSellMF:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSellOpt:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSellOther:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSellStock:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OOSwitchMF:
|
||||||
|
if err := e.EncodeElement(&oo, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("Invalid OOLIST child type: " + oo.OrderType())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(ooListElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ContribSecurity identifies current contribution allocation for a security in
|
// ContribSecurity identifies current contribution allocation for a security in
|
||||||
// a 401(k) account
|
// a 401(k) account
|
||||||
type ContribSecurity struct {
|
type ContribSecurity struct {
|
||||||
|
743
invstmt_test.go
743
invstmt_test.go
File diff suppressed because it is too large
Load Diff
@ -172,6 +172,7 @@ var ofxLeafElements = []string{
|
|||||||
"IDSCOPE",
|
"IDSCOPE",
|
||||||
"INCBAL",
|
"INCBAL",
|
||||||
"INCIMAGES",
|
"INCIMAGES",
|
||||||
|
"INCLUDE",
|
||||||
"INCOMETYPE",
|
"INCOMETYPE",
|
||||||
"INCOO",
|
"INCOO",
|
||||||
"INITIALAMT",
|
"INITIALAMT",
|
||||||
|
30
profile.go
30
profile.go
@ -3,6 +3,7 @@ package ofxgo
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/aclindsa/xml"
|
"github.com/aclindsa/xml"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProfileRequest represents a request for a server to provide a profile of its
|
// ProfileRequest represents a request for a server to provide a profile of its
|
||||||
@ -126,6 +127,35 @@ func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML handles marshalling a MessageSetList element to an XML string
|
||||||
|
func (msl *MessageSetList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
messageSetListElement := xml.StartElement{Name: xml.Name{Local: "MSGSETLIST"}}
|
||||||
|
if err := e.EncodeToken(messageSetListElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, messageset := range *msl {
|
||||||
|
if !strings.HasSuffix(messageset.Name, "V1") {
|
||||||
|
return errors.New("Expected MessageSet.Name to end with \"V1\"")
|
||||||
|
}
|
||||||
|
messageSetName := strings.TrimSuffix(messageset.Name, "V1")
|
||||||
|
messageSetElement := xml.StartElement{Name: xml.Name{Local: messageSetName}}
|
||||||
|
if err := e.EncodeToken(messageSetElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
start := xml.StartElement{Name: xml.Name{Local: messageset.Name}}
|
||||||
|
if err := e.EncodeElement(&messageset, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(messageSetElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(messageSetListElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProfileResponse contains a requested profile of the server's capabilities
|
// ProfileResponse contains a requested profile of the server's capabilities
|
||||||
// (which message sets and versions it supports, how to access them, which
|
// (which message sets and versions it supports, how to access them, which
|
||||||
// languages and which types of synchronization they support, etc.). Note that
|
// languages and which types of synchronization they support, etc.). Note that
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package ofxgo_test
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -36,13 +35,13 @@ func TestMarshalProfileRequest(t *testing.T) {
|
|||||||
</PROFMSGSRQV1>
|
</PROFMSGSRQV1>
|
||||||
</OFX>`
|
</OFX>`
|
||||||
|
|
||||||
var client = ofxgo.Client{
|
var client = BasicClient{
|
||||||
AppID: "OFXGO",
|
AppID: "OFXGO",
|
||||||
AppVer: "0001",
|
AppVer: "0001",
|
||||||
SpecVersion: ofxgo.OfxVersion203,
|
SpecVersion: OfxVersion203,
|
||||||
}
|
}
|
||||||
|
|
||||||
var request ofxgo.Request
|
var request Request
|
||||||
request.Signon.UserID = "anonymous00000000000000000000000"
|
request.Signon.UserID = "anonymous00000000000000000000000"
|
||||||
request.Signon.UserPass = "anonymous00000000000000000000000"
|
request.Signon.UserPass = "anonymous00000000000000000000000"
|
||||||
request.Signon.Org = "BNK"
|
request.Signon.Org = "BNK"
|
||||||
@ -50,15 +49,15 @@ func TestMarshalProfileRequest(t *testing.T) {
|
|||||||
|
|
||||||
EST := time.FixedZone("EST", -5*60*60)
|
EST := time.FixedZone("EST", -5*60*60)
|
||||||
|
|
||||||
profileRequest := ofxgo.ProfileRequest{
|
profileRequest := ProfileRequest{
|
||||||
TrnUID: "983373",
|
TrnUID: "983373",
|
||||||
DtProfUp: *ofxgo.NewDate(2016, 1, 1, 0, 0, 0, 0, EST),
|
DtProfUp: *NewDate(2016, 1, 1, 0, 0, 0, 0, EST),
|
||||||
}
|
}
|
||||||
request.Prof = append(request.Prof, &profileRequest)
|
request.Prof = append(request.Prof, &profileRequest)
|
||||||
|
|
||||||
request.SetClientFields(&client)
|
request.SetClientFields(&client)
|
||||||
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
||||||
request.Signon.DtClient = *ofxgo.NewDate(2016, 6, 14, 7, 34, 0, 0, EST)
|
request.Signon.DtClient = *NewDate(2016, 6, 14, 7, 34, 0, 0, EST)
|
||||||
|
|
||||||
marshalCheckRequest(t, &request, expectedString)
|
marshalCheckRequest(t, &request, expectedString)
|
||||||
}
|
}
|
||||||
@ -213,89 +212,89 @@ NEWFILEUID:NONE
|
|||||||
</PROFTRNRS>
|
</PROFTRNRS>
|
||||||
</PROFMSGSRSV1>
|
</PROFMSGSRSV1>
|
||||||
</OFX>`)
|
</OFX>`)
|
||||||
var expected ofxgo.Response
|
var expected Response
|
||||||
|
|
||||||
expected.Version = ofxgo.OfxVersion102
|
expected.Version = OfxVersion102
|
||||||
expected.Signon.Status.Code = 0
|
expected.Signon.Status.Code = 0
|
||||||
expected.Signon.Status.Severity = "INFO"
|
expected.Signon.Status.Severity = "INFO"
|
||||||
expected.Signon.DtServer = *ofxgo.NewDateGMT(2017, 4, 3, 9, 34, 58, 0)
|
expected.Signon.DtServer = *NewDateGMT(2017, 4, 3, 9, 34, 58, 0)
|
||||||
expected.Signon.Language = "ENG"
|
expected.Signon.Language = "ENG"
|
||||||
expected.Signon.DtProfUp = ofxgo.NewDateGMT(2002, 11, 19, 14, 0, 0, 0)
|
expected.Signon.DtProfUp = NewDateGMT(2002, 11, 19, 14, 0, 0, 0)
|
||||||
|
|
||||||
profileResponse := ofxgo.ProfileResponse{
|
profileResponse := ProfileResponse{
|
||||||
TrnUID: "0f94ce83-13b7-7568-e4fc-c02c7b47e7ab",
|
TrnUID: "0f94ce83-13b7-7568-e4fc-c02c7b47e7ab",
|
||||||
Status: ofxgo.Status{
|
Status: Status{
|
||||||
Code: 0,
|
Code: 0,
|
||||||
Severity: "INFO",
|
Severity: "INFO",
|
||||||
},
|
},
|
||||||
MessageSetList: ofxgo.MessageSetList{
|
MessageSetList: MessageSetList{
|
||||||
ofxgo.MessageSet{
|
MessageSet{
|
||||||
Name: "SIGNONMSGSETV1",
|
Name: "SIGNONMSGSETV1",
|
||||||
Ver: 1,
|
Ver: 1,
|
||||||
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
||||||
OfxSec: ofxgo.OfxSecNone,
|
OfxSec: OfxSecNone,
|
||||||
TranspSec: true,
|
TranspSec: true,
|
||||||
SignonRealm: "Example Trade",
|
SignonRealm: "Example Trade",
|
||||||
Language: []ofxgo.String{"ENG"},
|
Language: []String{"ENG"},
|
||||||
SyncMode: ofxgo.SyncModeLite,
|
SyncMode: SyncModeLite,
|
||||||
RespFileER: false,
|
RespFileER: false,
|
||||||
// Ignored: <INTU.TIMEOUT>300
|
// Ignored: <INTU.TIMEOUT>300
|
||||||
},
|
},
|
||||||
ofxgo.MessageSet{
|
MessageSet{
|
||||||
Name: "SIGNUPMSGSETV1",
|
Name: "SIGNUPMSGSETV1",
|
||||||
Ver: 1,
|
Ver: 1,
|
||||||
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
||||||
OfxSec: ofxgo.OfxSecNone,
|
OfxSec: OfxSecNone,
|
||||||
TranspSec: true,
|
TranspSec: true,
|
||||||
SignonRealm: "Example Trade",
|
SignonRealm: "Example Trade",
|
||||||
Language: []ofxgo.String{"ENG"},
|
Language: []String{"ENG"},
|
||||||
SyncMode: ofxgo.SyncModeLite,
|
SyncMode: SyncModeLite,
|
||||||
RespFileER: false,
|
RespFileER: false,
|
||||||
// Ignored: <INTU.TIMEOUT>300
|
// Ignored: <INTU.TIMEOUT>300
|
||||||
},
|
},
|
||||||
ofxgo.MessageSet{
|
MessageSet{
|
||||||
Name: "INVSTMTMSGSETV1",
|
Name: "INVSTMTMSGSETV1",
|
||||||
Ver: 1,
|
Ver: 1,
|
||||||
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
||||||
OfxSec: ofxgo.OfxSecNone,
|
OfxSec: OfxSecNone,
|
||||||
TranspSec: true,
|
TranspSec: true,
|
||||||
SignonRealm: "Example Trade",
|
SignonRealm: "Example Trade",
|
||||||
Language: []ofxgo.String{"ENG"},
|
Language: []String{"ENG"},
|
||||||
SyncMode: ofxgo.SyncModeLite,
|
SyncMode: SyncModeLite,
|
||||||
RespFileER: false,
|
RespFileER: false,
|
||||||
// Ignored: <INTU.TIMEOUT>300
|
// Ignored: <INTU.TIMEOUT>300
|
||||||
},
|
},
|
||||||
ofxgo.MessageSet{
|
MessageSet{
|
||||||
Name: "SECLISTMSGSETV1",
|
Name: "SECLISTMSGSETV1",
|
||||||
Ver: 1,
|
Ver: 1,
|
||||||
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
||||||
OfxSec: ofxgo.OfxSecNone,
|
OfxSec: OfxSecNone,
|
||||||
TranspSec: true,
|
TranspSec: true,
|
||||||
SignonRealm: "Example Trade",
|
SignonRealm: "Example Trade",
|
||||||
Language: []ofxgo.String{"ENG"},
|
Language: []String{"ENG"},
|
||||||
SyncMode: ofxgo.SyncModeLite,
|
SyncMode: SyncModeLite,
|
||||||
RespFileER: false,
|
RespFileER: false,
|
||||||
// Ignored: <INTU.TIMEOUT>300
|
// Ignored: <INTU.TIMEOUT>300
|
||||||
},
|
},
|
||||||
ofxgo.MessageSet{
|
MessageSet{
|
||||||
Name: "PROFMSGSETV1",
|
Name: "PROFMSGSETV1",
|
||||||
Ver: 1,
|
Ver: 1,
|
||||||
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
URL: "https://ofx.example.com/cgi-ofx/exampleofx",
|
||||||
OfxSec: ofxgo.OfxSecNone,
|
OfxSec: OfxSecNone,
|
||||||
TranspSec: true,
|
TranspSec: true,
|
||||||
SignonRealm: "Example Trade",
|
SignonRealm: "Example Trade",
|
||||||
Language: []ofxgo.String{"ENG"},
|
Language: []String{"ENG"},
|
||||||
SyncMode: ofxgo.SyncModeLite,
|
SyncMode: SyncModeLite,
|
||||||
RespFileER: false,
|
RespFileER: false,
|
||||||
// Ignored: <INTU.TIMEOUT>300
|
// Ignored: <INTU.TIMEOUT>300
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
SignonInfoList: []ofxgo.SignonInfo{
|
SignonInfoList: []SignonInfo{
|
||||||
{
|
{
|
||||||
SignonRealm: "Example Trade",
|
SignonRealm: "Example Trade",
|
||||||
Min: 1,
|
Min: 1,
|
||||||
Max: 32,
|
Max: 32,
|
||||||
CharType: ofxgo.CharTypeAlphaOrNumeric,
|
CharType: CharTypeAlphaOrNumeric,
|
||||||
CaseSen: false,
|
CaseSen: false,
|
||||||
Special: true,
|
Special: true,
|
||||||
Spaces: false,
|
Spaces: false,
|
||||||
@ -303,7 +302,7 @@ NEWFILEUID:NONE
|
|||||||
ChgPinFirst: false,
|
ChgPinFirst: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DtProfUp: *ofxgo.NewDateGMT(2002, 11, 19, 14, 0, 0, 0),
|
DtProfUp: *NewDateGMT(2002, 11, 19, 14, 0, 0, 0),
|
||||||
FiName: "Example Trade Financial",
|
FiName: "Example Trade Financial",
|
||||||
Addr1: "5555 Buhunkus Drive",
|
Addr1: "5555 Buhunkus Drive",
|
||||||
City: "Someville",
|
City: "Someville",
|
||||||
@ -319,10 +318,11 @@ NEWFILEUID:NONE
|
|||||||
}
|
}
|
||||||
expected.Prof = append(expected.Prof, &profileResponse)
|
expected.Prof = append(expected.Prof, &profileResponse)
|
||||||
|
|
||||||
response, err := ofxgo.ParseResponse(responseReader)
|
response, err := ParseResponse(responseReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkResponsesEqual(t, &expected, response)
|
checkResponsesEqual(t, &expected, response)
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
}
|
}
|
||||||
|
36
request.go
36
request.go
@ -3,7 +3,6 @@ package ofxgo
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/aclindsa/xml"
|
"github.com/aclindsa/xml"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -33,9 +32,10 @@ type Request struct {
|
|||||||
Image []Message //<IMAGEMSGSETV1>
|
Image []Message //<IMAGEMSGSETV1>
|
||||||
|
|
||||||
indent bool // Whether to indent the marshaled XML
|
indent bool // Whether to indent the marshaled XML
|
||||||
|
carriageReturn bool // Whether to user carriage returns in new lines for marshaled XML
|
||||||
}
|
}
|
||||||
|
|
||||||
func marshalMessageSet(e *xml.Encoder, requests []Message, set messageType, version ofxVersion) error {
|
func encodeMessageSet(e *xml.Encoder, requests []Message, set messageType, version ofxVersion) error {
|
||||||
if len(requests) > 0 {
|
if len(requests) > 0 {
|
||||||
messageSetElement := xml.StartElement{Name: xml.Name{Local: set.String()}}
|
messageSetElement := xml.StartElement{Name: xml.Name{Local: set.String()}}
|
||||||
if err := e.EncodeToken(messageSetElement); err != nil {
|
if err := e.EncodeToken(messageSetElement); err != nil {
|
||||||
@ -63,7 +63,7 @@ func marshalMessageSet(e *xml.Encoder, requests []Message, set messageType, vers
|
|||||||
|
|
||||||
// SetClientFields overwrites the fields in this Request object controlled by
|
// SetClientFields overwrites the fields in this Request object controlled by
|
||||||
// the Client
|
// the Client
|
||||||
func (oq *Request) SetClientFields(c *Client) {
|
func (oq *Request) SetClientFields(c Client) {
|
||||||
oq.Signon.DtClient.Time = time.Now()
|
oq.Signon.DtClient.Time = time.Now()
|
||||||
|
|
||||||
// Overwrite fields that the client controls
|
// Overwrite fields that the client controls
|
||||||
@ -71,6 +71,7 @@ func (oq *Request) SetClientFields(c *Client) {
|
|||||||
oq.Signon.AppID = c.ID()
|
oq.Signon.AppID = c.ID()
|
||||||
oq.Signon.AppVer = c.Version()
|
oq.Signon.AppVer = c.Version()
|
||||||
oq.indent = c.IndentRequests()
|
oq.indent = c.IndentRequests()
|
||||||
|
oq.carriageReturn = c.CarriageReturnNewLines()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal this Request into its SGML/XML representation held in a bytes.Buffer
|
// Marshal this Request into its SGML/XML representation held in a bytes.Buffer
|
||||||
@ -80,30 +81,19 @@ func (oq *Request) Marshal() (*bytes.Buffer, error) {
|
|||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
|
|
||||||
// Write the header appropriate to our version
|
// Write the header appropriate to our version
|
||||||
switch oq.Version {
|
writeHeader(&b, oq.Version, oq.carriageReturn)
|
||||||
case OfxVersion102, OfxVersion103, OfxVersion151, OfxVersion160:
|
|
||||||
b.WriteString(`OFXHEADER:100
|
|
||||||
DATA:OFXSGML
|
|
||||||
VERSION:` + oq.Version.String() + `
|
|
||||||
SECURITY:NONE
|
|
||||||
ENCODING:USASCII
|
|
||||||
CHARSET:1252
|
|
||||||
COMPRESSION:NONE
|
|
||||||
OLDFILEUID:NONE
|
|
||||||
NEWFILEUID:NONE
|
|
||||||
|
|
||||||
`)
|
|
||||||
case OfxVersion200, OfxVersion201, OfxVersion202, OfxVersion203, OfxVersion210, OfxVersion211, OfxVersion220:
|
|
||||||
b.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + "\n")
|
|
||||||
b.WriteString(`<?OFX OFXHEADER="200" VERSION="` + oq.Version.String() + `" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>` + "\n")
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%d is not a valid OFX version string", oq.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
encoder := xml.NewEncoder(&b)
|
encoder := xml.NewEncoder(&b)
|
||||||
if oq.indent {
|
if oq.indent {
|
||||||
encoder.Indent("", " ")
|
encoder.Indent("", " ")
|
||||||
}
|
}
|
||||||
|
if oq.carriageReturn {
|
||||||
|
encoder.CarriageReturn(true)
|
||||||
|
}
|
||||||
|
if oq.Version < OfxVersion200 {
|
||||||
|
// OFX 100 series versions should avoid element close tags for compatibility
|
||||||
|
encoder.SetDisableAutoClose(ofxLeafElements...)
|
||||||
|
}
|
||||||
|
|
||||||
ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}}
|
ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}}
|
||||||
|
|
||||||
@ -145,7 +135,7 @@ NEWFILEUID:NONE
|
|||||||
{oq.Image, ImageRq},
|
{oq.Image, ImageRq},
|
||||||
}
|
}
|
||||||
for _, set := range messageSets {
|
for _, set := range messageSets {
|
||||||
if err := marshalMessageSet(encoder, set.Messages, set.Type, oq.Version); err != nil {
|
if err := encodeMessageSet(encoder, set.Messages, set.Type, oq.Version); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
package ofxgo_test
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ignoreSpacesRe = regexp.MustCompile(">[ \t\r\n]+<")
|
// match leading and trailing whitespace on each line
|
||||||
|
var ignoreSpacesRe = regexp.MustCompile("(?m)^[ \t]+|[ \t]*$[\r\n]+")
|
||||||
|
|
||||||
func marshalCheckRequest(t *testing.T, request *ofxgo.Request, expected string) {
|
func marshalCheckRequest(t *testing.T, request *Request, expected string) {
|
||||||
|
t.Helper()
|
||||||
buf, err := request.Marshal()
|
buf, err := request.Marshal()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%s: Unexpected error marshalling request: %s\n", t.Name(), err)
|
t.Fatalf("%s: Unexpected error marshalling request: %s\n", t.Name(), err)
|
||||||
@ -17,8 +18,8 @@ func marshalCheckRequest(t *testing.T, request *ofxgo.Request, expected string)
|
|||||||
actualString := buf.String()
|
actualString := buf.String()
|
||||||
|
|
||||||
// Ignore spaces between XML elements
|
// Ignore spaces between XML elements
|
||||||
expectedString := ignoreSpacesRe.ReplaceAllString(expected, "><")
|
expectedString := ignoreSpacesRe.ReplaceAllString(expected, "")
|
||||||
actualString = ignoreSpacesRe.ReplaceAllString(actualString, "><")
|
actualString = ignoreSpacesRe.ReplaceAllString(actualString, "")
|
||||||
|
|
||||||
if expectedString != actualString {
|
if expectedString != actualString {
|
||||||
compareLength := len(expectedString)
|
compareLength := len(expectedString)
|
||||||
|
224
response.go
224
response.go
@ -4,10 +4,13 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/aclindsa/xml"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aclindsa/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Response is the top-level object returned from a parsed OFX response file.
|
// Response is the top-level object returned from a parsed OFX response file.
|
||||||
@ -33,68 +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 {
|
|
||||||
line, err := r.ReadString('\n')
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// r.ReadString leaves the '\n' on the end...
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
|
|
||||||
if len(line) == 0 {
|
s := string(b)
|
||||||
if seenHeader {
|
err = r.UnreadByte()
|
||||||
break
|
if err != nil {
|
||||||
} else {
|
return err
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
header := strings.SplitN(line, ":", 2)
|
// According to the latest OFX SGML spec (1.6), headers should be CRLF-separated
|
||||||
if header == nil || len(header) != 2 {
|
// 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")
|
return errors.New("OFX headers malformed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some OFX servers put a space after the colon
|
for i, name := range headerExp.SubexpNames() {
|
||||||
headervalue := strings.TrimSpace(header[1])
|
if i == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
switch header[0] {
|
headerValue := matches[i]
|
||||||
|
switch name {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,6 +185,7 @@ const guessVersionCheckBytes = 1024
|
|||||||
|
|
||||||
// Defaults to XML if it can't determine the version or if there is any
|
// Defaults to XML if it can't determine the version or if there is any
|
||||||
// ambiguity
|
// ambiguity
|
||||||
|
// Returns false for SGML, true (for XML) otherwise.
|
||||||
func guessVersion(r *bufio.Reader) (bool, error) {
|
func guessVersion(r *bufio.Reader) (bool, error) {
|
||||||
b, _ := r.Peek(guessVersionCheckBytes)
|
b, _ := r.Peek(guessVersionCheckBytes)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
@ -245,9 +256,6 @@ func decodeMessageSet(d *xml.Decoder, start xml.StartElement, msgs *[]Message, v
|
|||||||
if err := d.DecodeElement(responseMessage, &startElement); err != nil {
|
if err := d.DecodeElement(responseMessage, &startElement); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if ok, err := responseMessage.Valid(version); !ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*msgs = append(*msgs, responseMessage)
|
*msgs = append(*msgs, responseMessage)
|
||||||
} else {
|
} else {
|
||||||
return errors.New("Didn't find an opening element")
|
return errors.New("Didn't find an opening element")
|
||||||
@ -255,14 +263,25 @@ func decodeMessageSet(d *xml.Decoder, start xml.StartElement, msgs *[]Message, v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseResponse parses an OFX response in SGML or XML into a Response object
|
// ParseResponse parses and validates an OFX response in SGML or XML into a
|
||||||
// from the given io.Reader
|
// Response object from the given io.Reader
|
||||||
//
|
//
|
||||||
// It is commonly used as part of Client.Request(), but may be used on its own
|
// It is commonly used as part of Client.Request(), but may be used on its own
|
||||||
// to parse already-downloaded OFX files (such as those from 'Web Connect'). It
|
// to parse already-downloaded OFX files (such as those from 'Web Connect'). It
|
||||||
// performs version autodetection if it can and attempts to be as forgiving as
|
// performs version autodetection if it can and attempts to be as forgiving as
|
||||||
// possible about the input format.
|
// possible about the input format.
|
||||||
func ParseResponse(reader io.Reader) (*Response, error) {
|
func ParseResponse(reader io.Reader) (*Response, error) {
|
||||||
|
resp, err := DecodeResponse(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = resp.Valid()
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeResponse parses an OFX response in SGML or XML into a Response object
|
||||||
|
// from the given io.Reader
|
||||||
|
func DecodeResponse(reader io.Reader) (*Response, error) {
|
||||||
var or Response
|
var or Response
|
||||||
|
|
||||||
r := bufio.NewReaderSize(reader, guessVersionCheckBytes)
|
r := bufio.NewReaderSize(reader, guessVersionCheckBytes)
|
||||||
@ -319,9 +338,6 @@ func ParseResponse(reader io.Reader) (*Response, error) {
|
|||||||
} else if signonEnd, ok := tok.(xml.EndElement); !ok || signonEnd.Name.Local != SignonRs.String() {
|
} else if signonEnd, ok := tok.(xml.EndElement); !ok || signonEnd.Name.Local != SignonRs.String() {
|
||||||
return nil, errors.New("Missing closing SIGNONMSGSRSV1 xml element")
|
return nil, errors.New("Missing closing SIGNONMSGSRSV1 xml element")
|
||||||
}
|
}
|
||||||
if ok, err := or.Signon.Valid(or.Version); !ok {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageSlices = map[string]*[]Message{
|
var messageSlices = map[string]*[]Message{
|
||||||
SignupRs.String(): &or.Signup,
|
SignupRs.String(): &or.Signup,
|
||||||
@ -359,3 +375,131 @@ func ParseResponse(reader io.Reader) (*Response, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Valid returns whether the Response is valid according to the OFX spec
|
||||||
|
func (or *Response) Valid() (bool, error) {
|
||||||
|
var errs errInvalid
|
||||||
|
if ok, err := or.Signon.Valid(or.Version); !ok {
|
||||||
|
errs.AddErr(err)
|
||||||
|
}
|
||||||
|
for _, messageSet := range [][]Message{
|
||||||
|
or.Signup,
|
||||||
|
or.Bank,
|
||||||
|
or.CreditCard,
|
||||||
|
or.Loan,
|
||||||
|
or.InvStmt,
|
||||||
|
or.InterXfer,
|
||||||
|
or.WireXfer,
|
||||||
|
or.Billpay,
|
||||||
|
or.Email,
|
||||||
|
or.SecList,
|
||||||
|
or.PresDir,
|
||||||
|
or.PresDlv,
|
||||||
|
or.Prof,
|
||||||
|
or.Image,
|
||||||
|
} {
|
||||||
|
for _, message := range messageSet {
|
||||||
|
if ok, err := message.Valid(or.Version); !ok {
|
||||||
|
errs.AddErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := errs.ErrOrNil()
|
||||||
|
return err == nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal this Response into its SGML/XML representation held in a bytes.Buffer
|
||||||
|
//
|
||||||
|
// If error is non-nil, this bytes.Buffer is ready to be sent to an OFX client
|
||||||
|
func (or *Response) Marshal() (*bytes.Buffer, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
// Write the header appropriate to our version
|
||||||
|
writeHeader(&b, or.Version, false)
|
||||||
|
|
||||||
|
encoder := xml.NewEncoder(&b)
|
||||||
|
encoder.Indent("", " ")
|
||||||
|
|
||||||
|
ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}}
|
||||||
|
|
||||||
|
if err := encoder.EncodeToken(ofxElement); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := or.Signon.Valid(or.Version); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signonMsgSet := xml.StartElement{Name: xml.Name{Local: SignonRs.String()}}
|
||||||
|
if err := encoder.EncodeToken(signonMsgSet); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := encoder.Encode(&or.Signon); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := encoder.EncodeToken(signonMsgSet.End()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
messageSets := []struct {
|
||||||
|
Messages []Message
|
||||||
|
Type messageType
|
||||||
|
}{
|
||||||
|
{or.Signup, SignupRs},
|
||||||
|
{or.Bank, BankRs},
|
||||||
|
{or.CreditCard, CreditCardRs},
|
||||||
|
{or.Loan, LoanRs},
|
||||||
|
{or.InvStmt, InvStmtRs},
|
||||||
|
{or.InterXfer, InterXferRs},
|
||||||
|
{or.WireXfer, WireXferRs},
|
||||||
|
{or.Billpay, BillpayRs},
|
||||||
|
{or.Email, EmailRs},
|
||||||
|
{or.SecList, SecListRs},
|
||||||
|
{or.PresDir, PresDirRs},
|
||||||
|
{or.PresDlv, PresDlvRs},
|
||||||
|
{or.Prof, ProfRs},
|
||||||
|
{or.Image, ImageRs},
|
||||||
|
}
|
||||||
|
for _, set := range messageSets {
|
||||||
|
if err := encodeMessageSet(encoder, set.Messages, set.Type, or.Version); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := encoder.EncodeToken(ofxElement.End()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := encoder.Flush(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// errInvalid represents validation failures while parsing an OFX response
|
||||||
|
// If an institution returns slightly malformed data, ParseResponse will return a best-effort parsed response and a validation error.
|
||||||
|
type errInvalid []error
|
||||||
|
|
||||||
|
func (e errInvalid) Error() string {
|
||||||
|
var errStrings []string
|
||||||
|
for _, err := range e {
|
||||||
|
errStrings = append(errStrings, err.Error())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Validation failed: %s", strings.Join(errStrings, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *errInvalid) AddErr(err error) {
|
||||||
|
if err != nil {
|
||||||
|
if errs, ok := err.(errInvalid); ok {
|
||||||
|
*e = append(*e, errs...)
|
||||||
|
} else {
|
||||||
|
*e = append(*e, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e errInvalid) ErrOrNil() error {
|
||||||
|
if len(e) > 0 {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
179
response_test.go
179
response_test.go
@ -1,13 +1,15 @@
|
|||||||
package ofxgo_test
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/aclindsa/xml"
|
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aclindsa/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Attempt to find a method on the provided Value called 'Equal' which is a
|
// Attempt to find a method on the provided Value called 'Equal' which is a
|
||||||
@ -132,27 +134,190 @@ func checkEqual(t *testing.T, fieldName string, expected, actual reflect.Value)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkResponsesEqual(t *testing.T, expected, actual *ofxgo.Response) {
|
func checkResponsesEqual(t *testing.T, expected, actual *Response) {
|
||||||
checkEqual(t, "", reflect.ValueOf(expected), reflect.ValueOf(actual))
|
checkEqual(t, "", reflect.ValueOf(expected), reflect.ValueOf(actual))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkResponseRoundTrip(t *testing.T, response *Response) {
|
||||||
|
b, err := response.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error re-marshaling OFX response: %s\n", err)
|
||||||
|
}
|
||||||
|
roundtripped, err := ParseResponse(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error re-parsing OFX response: %s\n", err)
|
||||||
|
}
|
||||||
|
checkResponsesEqual(t, response, roundtripped)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that these samples both parse without errors, and can be converted
|
||||||
|
// back and forth without changing.
|
||||||
func TestValidSamples(t *testing.T) {
|
func TestValidSamples(t *testing.T) {
|
||||||
fn := func(path string, info os.FileInfo, err error) error {
|
fn := func(path string, info os.FileInfo, err error) error {
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
} else if filepath.Ext(path) != ".ofx" {
|
} else if ext := filepath.Ext(path); ext != ".ofx" && ext != ".qfx" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error opening %s: %s\n", path, err)
|
t.Fatalf("Unexpected error opening %s: %s\n", path, err)
|
||||||
}
|
}
|
||||||
_, err = ofxgo.ParseResponse(file)
|
response, err := ParseResponse(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error parsing OFX response in %s: %s\n", path, err)
|
t.Fatalf("Unexpected error parsing OFX response in %s: %s\n", path, err)
|
||||||
}
|
}
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
filepath.Walk("samples/valid_responses", fn)
|
filepath.Walk("samples/valid_responses", fn)
|
||||||
|
filepath.Walk("samples/busted_responses", fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidResponse(t *testing.T) {
|
||||||
|
// in this example, the severity is invalid due to mixed upper and lower case letters
|
||||||
|
const invalidResponse = `OFXHEADER:100
|
||||||
|
DATA:OFXSGML
|
||||||
|
VERSION:102
|
||||||
|
SECURITY:NONE
|
||||||
|
ENCODING:USASCII
|
||||||
|
CHARSET:1252
|
||||||
|
COMPRESSION:NONE
|
||||||
|
OLDFILEUID:NONE
|
||||||
|
NEWFILEUID:NONE
|
||||||
|
|
||||||
|
<OFX>
|
||||||
|
<SIGNONMSGSRSV1>
|
||||||
|
<SONRS>
|
||||||
|
<STATUS>
|
||||||
|
<CODE>0</CODE>
|
||||||
|
<SEVERITY>Info</SEVERITY>
|
||||||
|
</STATUS>
|
||||||
|
<LANGUAGE>ENG</LANGUAGE>
|
||||||
|
</SONRS>
|
||||||
|
</SIGNONMSGSRSV1>
|
||||||
|
<BANKMSGSRSV1>
|
||||||
|
<STMTTRNRS>
|
||||||
|
<TRNUID>0</TRNUID>
|
||||||
|
<STATUS>
|
||||||
|
<CODE>0</CODE>
|
||||||
|
<SEVERITY>Info</SEVERITY>
|
||||||
|
</STATUS>
|
||||||
|
</STMTTRNRS>
|
||||||
|
</BANKMSGSRSV1>
|
||||||
|
</OFX>
|
||||||
|
`
|
||||||
|
const expectedErr = "Validation failed: Invalid STATUS>SEVERITY; Invalid STATUS>SEVERITY"
|
||||||
|
|
||||||
|
t.Run("parse response", func(t *testing.T) {
|
||||||
|
resp, err := ParseResponse(bytes.NewReader([]byte(invalidResponse)))
|
||||||
|
expectedErr := "Validation failed: Invalid STATUS>SEVERITY; Invalid STATUS>SEVERITY"
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("ParseResponse should fail with %q, found nil", expectedErr)
|
||||||
|
}
|
||||||
|
if _, ok := err.(errInvalid); !ok {
|
||||||
|
t.Errorf("ParseResponse should return an error with type ErrInvalid, found %T", err)
|
||||||
|
}
|
||||||
|
if err.Error() != expectedErr {
|
||||||
|
t.Errorf("ParseResponse should fail with %q, found %v", expectedErr, err)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Errorf("Response must not be nil if only validation errors are present")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parse failed", func(t *testing.T) {
|
||||||
|
resp, err := ParseResponse(bytes.NewReader(nil))
|
||||||
|
if err == nil {
|
||||||
|
t.Error("ParseResponse should fail to decode")
|
||||||
|
}
|
||||||
|
if resp != nil {
|
||||||
|
t.Errorf("ParseResponse should return a nil response, found: %v", resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("decode, then validate response", func(t *testing.T) {
|
||||||
|
resp, err := DecodeResponse(bytes.NewReader([]byte(invalidResponse)))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %s", err.Error())
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("Response should not be nil from successful decode")
|
||||||
|
}
|
||||||
|
valid, err := resp.Valid()
|
||||||
|
if valid {
|
||||||
|
t.Error("Response should not be valid")
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("response.Valid() should fail with %q, found nil", expectedErr)
|
||||||
|
}
|
||||||
|
if _, ok := err.(errInvalid); !ok {
|
||||||
|
t.Errorf("response.Valid() should return an error of type ErrInvalid, found: %T", err)
|
||||||
|
}
|
||||||
|
if err.Error() != expectedErr {
|
||||||
|
t.Errorf("response.Valid() should return an error with message %q, but found %q", expectedErr, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrInvalidError(t *testing.T) {
|
||||||
|
expectedErr := `Validation failed: A; B; C`
|
||||||
|
actualErr := errInvalid{
|
||||||
|
errors.New("A"),
|
||||||
|
errors.New("B"),
|
||||||
|
errors.New("C"),
|
||||||
|
}.Error()
|
||||||
|
if expectedErr != actualErr {
|
||||||
|
t.Errorf("Unexpected invalid error message to be %q, but was: %s", expectedErr, actualErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrInvalidAddErr(t *testing.T) {
|
||||||
|
t.Run("nil error should be a no-op", func(t *testing.T) {
|
||||||
|
var errs errInvalid
|
||||||
|
errs.AddErr(nil)
|
||||||
|
if len(errs) != 0 {
|
||||||
|
t.Errorf("Nil err should not be added")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adds an error normally", func(t *testing.T) {
|
||||||
|
var errs errInvalid
|
||||||
|
errs.AddErr(errors.New("some error"))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adding the same type should flatten the errors", func(t *testing.T) {
|
||||||
|
var errs errInvalid
|
||||||
|
errs.AddErr(errInvalid{
|
||||||
|
errors.New("A"),
|
||||||
|
errors.New("B"),
|
||||||
|
})
|
||||||
|
errs.AddErr(errInvalid{
|
||||||
|
errors.New("C"),
|
||||||
|
})
|
||||||
|
if len(errs) != 3 {
|
||||||
|
t.Errorf("Errors should be flattened like [A, B, C], but found: %+v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrInvalidErrOrNil(t *testing.T) {
|
||||||
|
var errs errInvalid
|
||||||
|
if err := errs.ErrOrNil(); err != nil {
|
||||||
|
t.Errorf("No added errors should return nil, found: %v", err)
|
||||||
|
}
|
||||||
|
someError := errors.New("some error")
|
||||||
|
errs.AddErr(someError)
|
||||||
|
err := errs.ErrOrNil()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected an error, found nil.")
|
||||||
|
}
|
||||||
|
if _, ok := err.(errInvalid); !ok {
|
||||||
|
t.Fatalf("Expected err to be of type errInvalid, found: %T", err)
|
||||||
|
}
|
||||||
|
errInv := err.(errInvalid)
|
||||||
|
if len(errInv) != 1 || errInv[0] != someError {
|
||||||
|
t.Errorf("Expected ErrOrNil to return itself, found: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
59
samples/busted_responses/bmo_v102__no_header_newline.qfx
Normal file
59
samples/busted_responses/bmo_v102__no_header_newline.qfx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
OFXHEADER:100
|
||||||
|
DATA:OFXSGML
|
||||||
|
VERSION:102
|
||||||
|
SECURITY:NONE
|
||||||
|
ENCODING:USASCII
|
||||||
|
CHARSET:1252
|
||||||
|
COMPRESSION:NONE
|
||||||
|
OLDFILEUID:NONE
|
||||||
|
NEWFILEUID:NONE
|
||||||
|
<OFX>
|
||||||
|
<SIGNONMSGSRSV1>
|
||||||
|
<SONRS>
|
||||||
|
<STATUS>
|
||||||
|
<CODE>0
|
||||||
|
<SEVERITY>INFO
|
||||||
|
<MESSAGE>OK
|
||||||
|
</STATUS>
|
||||||
|
<DTSERVER>20181202184906.217[-5:EDT]
|
||||||
|
<USERKEY>SJLDF802DV09DF80
|
||||||
|
<LANGUAGE>ENG
|
||||||
|
<INTU.BID>00017
|
||||||
|
</SONRS>
|
||||||
|
</SIGNONMSGSRSV1>
|
||||||
|
<CREDITCARDMSGSRSV1>
|
||||||
|
<CCSTMTTRNRS>
|
||||||
|
<TRNUID>1
|
||||||
|
<STATUS>
|
||||||
|
<CODE>0
|
||||||
|
<SEVERITY>INFO
|
||||||
|
<MESSAGE>OK
|
||||||
|
</STATUS>
|
||||||
|
<CCSTMTRS>
|
||||||
|
<CURDEF>CAD
|
||||||
|
<CCACCTFROM>
|
||||||
|
<ACCTID>2380370270281083
|
||||||
|
</CCACCTFROM>
|
||||||
|
<BANKTRANLIST>
|
||||||
|
<DTSTART>20181202184905.909[-5:EDT]
|
||||||
|
<DTEND>20181202184905.909[-5:EDT]
|
||||||
|
<STMTTRN>
|
||||||
|
<TRNTYPE>CREDIT
|
||||||
|
<DTPOSTED>20181030000000.000[-5:EDT]
|
||||||
|
<TRNAMT>2042.24
|
||||||
|
<FITID>2380370270281083201810302054456
|
||||||
|
<NAME>PAYMENT RECEIVED - THANK YOU
|
||||||
|
</STMTTRN>
|
||||||
|
</BANKTRANLIST>
|
||||||
|
<LEDGERBAL>
|
||||||
|
<BALAMT>-552.63
|
||||||
|
<DTASOF>20181202184906.217[-5:EDT]
|
||||||
|
</LEDGERBAL>
|
||||||
|
<AVAILBAL>
|
||||||
|
<BALAMT>-552.63
|
||||||
|
<DTASOF>20181202184906.217[-5:EDT]
|
||||||
|
</AVAILBAL>
|
||||||
|
</CCSTMTRS>
|
||||||
|
</CCSTMTTRNRS>
|
||||||
|
</CREDITCARDMSGSRSV1>
|
||||||
|
</OFX>
|
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>
|
40
seclist.go
40
seclist.go
@ -221,6 +221,7 @@ func (i StockInfo) SecurityType() string {
|
|||||||
// SecurityList is a container for Security objects containaing information
|
// SecurityList is a container for Security objects containaing information
|
||||||
// about securities
|
// about securities
|
||||||
type SecurityList struct {
|
type SecurityList struct {
|
||||||
|
XMLName xml.Name `xml:"SECLIST"`
|
||||||
Securities []Security
|
Securities []Security
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,3 +291,42 @@ func (r *SecurityList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalXML handles marshalling a SecurityList to an SGML/XML string
|
||||||
|
func (r *SecurityList) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
secListElement := xml.StartElement{Name: xml.Name{Local: "SECLIST"}}
|
||||||
|
if err := e.EncodeToken(secListElement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, s := range r.Securities {
|
||||||
|
start := xml.StartElement{Name: xml.Name{Local: s.SecurityType()}}
|
||||||
|
switch sec := s.(type) {
|
||||||
|
case DebtInfo:
|
||||||
|
if err := e.EncodeElement(&sec, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case MFInfo:
|
||||||
|
if err := e.EncodeElement(&sec, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OptInfo:
|
||||||
|
if err := e.EncodeElement(&sec, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case OtherInfo:
|
||||||
|
if err := e.EncodeElement(&sec, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case StockInfo:
|
||||||
|
if err := e.EncodeElement(&sec, start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("Invalid SECLIST child type: " + sec.SecurityType())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(secListElement.End()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -14,6 +14,7 @@ type SignonRequest struct {
|
|||||||
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"`
|
||||||
|
GenUserKey Boolean `xml:"GENUSERKEY,omitempty"`
|
||||||
Language String `xml:"LANGUAGE"` // Defaults to ENG
|
Language String `xml:"LANGUAGE"` // Defaults to ENG
|
||||||
Org String `xml:"FI>ORG"`
|
Org String `xml:"FI>ORG"`
|
||||||
Fid String `xml:"FI>FID"`
|
Fid String `xml:"FI>FID"`
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
package ofxgo_test
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMarshalInvalidSignons(t *testing.T) {
|
func TestMarshalInvalidSignons(t *testing.T) {
|
||||||
var client = ofxgo.Client{
|
var client = BasicClient{
|
||||||
AppID: "OFXGO",
|
AppID: "OFXGO",
|
||||||
AppVer: "0001",
|
AppVer: "0001",
|
||||||
SpecVersion: ofxgo.OfxVersion203,
|
SpecVersion: OfxVersion203,
|
||||||
}
|
}
|
||||||
|
|
||||||
var request ofxgo.Request
|
var request Request
|
||||||
request.Signon.UserID = "myusername"
|
request.Signon.UserID = "myusername"
|
||||||
request.Signon.UserPass = "Pa$$word"
|
request.Signon.UserPass = "Pa$$word"
|
||||||
request.Signon.Org = "BNK"
|
request.Signon.Org = "BNK"
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package ofxgo_test
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -37,27 +36,27 @@ func TestMarshalAcctInfoRequest(t *testing.T) {
|
|||||||
|
|
||||||
EST := time.FixedZone("EST", -5*60*60)
|
EST := time.FixedZone("EST", -5*60*60)
|
||||||
|
|
||||||
var client = ofxgo.Client{
|
var client = BasicClient{
|
||||||
AppID: "OFXGO",
|
AppID: "OFXGO",
|
||||||
AppVer: "0001",
|
AppVer: "0001",
|
||||||
SpecVersion: ofxgo.OfxVersion203,
|
SpecVersion: OfxVersion203,
|
||||||
}
|
}
|
||||||
|
|
||||||
var request ofxgo.Request
|
var request Request
|
||||||
request.Signon.UserID = "myusername"
|
request.Signon.UserID = "myusername"
|
||||||
request.Signon.UserPass = "Pa$$word"
|
request.Signon.UserPass = "Pa$$word"
|
||||||
request.Signon.Org = "BNK"
|
request.Signon.Org = "BNK"
|
||||||
request.Signon.Fid = "1987"
|
request.Signon.Fid = "1987"
|
||||||
|
|
||||||
acctInfoRequest := ofxgo.AcctInfoRequest{
|
acctInfoRequest := AcctInfoRequest{
|
||||||
TrnUID: "e3ad9bda-38fa-4e5b-8099-1bd567ddef7a",
|
TrnUID: "e3ad9bda-38fa-4e5b-8099-1bd567ddef7a",
|
||||||
DtAcctUp: *ofxgo.NewDate(2015, 12, 21, 18, 29, 45, 0, EST),
|
DtAcctUp: *NewDate(2015, 12, 21, 18, 29, 45, 0, EST),
|
||||||
}
|
}
|
||||||
request.Signup = append(request.Signup, &acctInfoRequest)
|
request.Signup = append(request.Signup, &acctInfoRequest)
|
||||||
|
|
||||||
request.SetClientFields(&client)
|
request.SetClientFields(&client)
|
||||||
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
// Overwrite the DtClient value set by SetClientFields to time.Now()
|
||||||
request.Signon.DtClient = *ofxgo.NewDate(2016, 1, 15, 11, 23, 0, 0, EST)
|
request.Signon.DtClient = *NewDate(2016, 1, 15, 11, 23, 0, 0, EST)
|
||||||
|
|
||||||
marshalCheckRequest(t, &request, expectedString)
|
marshalCheckRequest(t, &request, expectedString)
|
||||||
}
|
}
|
||||||
@ -110,38 +109,38 @@ func TestUnmarshalAcctInfoResponse(t *testing.T) {
|
|||||||
</ACCTINFOTRNRS>
|
</ACCTINFOTRNRS>
|
||||||
</SIGNUPMSGSRSV1>
|
</SIGNUPMSGSRSV1>
|
||||||
</OFX>`)
|
</OFX>`)
|
||||||
var expected ofxgo.Response
|
var expected Response
|
||||||
|
|
||||||
expected.Version = ofxgo.OfxVersion203
|
expected.Version = OfxVersion203
|
||||||
expected.Signon.Status.Code = 0
|
expected.Signon.Status.Code = 0
|
||||||
expected.Signon.Status.Severity = "INFO"
|
expected.Signon.Status.Severity = "INFO"
|
||||||
expected.Signon.DtServer = *ofxgo.NewDateGMT(2006, 1, 15, 11, 23, 03, 0)
|
expected.Signon.DtServer = *NewDateGMT(2006, 1, 15, 11, 23, 03, 0)
|
||||||
expected.Signon.Language = "ENG"
|
expected.Signon.Language = "ENG"
|
||||||
expected.Signon.DtProfUp = ofxgo.NewDateGMT(2005, 2, 21, 9, 13, 0, 0)
|
expected.Signon.DtProfUp = NewDateGMT(2005, 2, 21, 9, 13, 0, 0)
|
||||||
expected.Signon.DtAcctUp = ofxgo.NewDateGMT(2006, 1, 2, 16, 0, 0, 0)
|
expected.Signon.DtAcctUp = NewDateGMT(2006, 1, 2, 16, 0, 0, 0)
|
||||||
expected.Signon.Org = "BNK"
|
expected.Signon.Org = "BNK"
|
||||||
expected.Signon.Fid = "1987"
|
expected.Signon.Fid = "1987"
|
||||||
|
|
||||||
bankacctinfo := ofxgo.BankAcctInfo{
|
bankacctinfo := BankAcctInfo{
|
||||||
BankAcctFrom: ofxgo.BankAcct{
|
BankAcctFrom: BankAcct{
|
||||||
BankID: "8367556009",
|
BankID: "8367556009",
|
||||||
AcctID: "000999847",
|
AcctID: "000999847",
|
||||||
AcctType: ofxgo.AcctTypeMoneyMrkt,
|
AcctType: AcctTypeMoneyMrkt,
|
||||||
},
|
},
|
||||||
SupTxDl: true,
|
SupTxDl: true,
|
||||||
XferSrc: true,
|
XferSrc: true,
|
||||||
XferDest: true,
|
XferDest: true,
|
||||||
SvcStatus: ofxgo.SvcStatusActive,
|
SvcStatus: SvcStatusActive,
|
||||||
}
|
}
|
||||||
|
|
||||||
acctInfoResponse := ofxgo.AcctInfoResponse{
|
acctInfoResponse := AcctInfoResponse{
|
||||||
TrnUID: "10938754",
|
TrnUID: "10938754",
|
||||||
Status: ofxgo.Status{
|
Status: Status{
|
||||||
Code: 0,
|
Code: 0,
|
||||||
Severity: "INFO",
|
Severity: "INFO",
|
||||||
},
|
},
|
||||||
DtAcctUp: *ofxgo.NewDateGMT(2005, 2, 28, 0, 0, 0, 0),
|
DtAcctUp: *NewDateGMT(2005, 2, 28, 0, 0, 0, 0),
|
||||||
AcctInfo: []ofxgo.AcctInfo{{
|
AcctInfo: []AcctInfo{{
|
||||||
Desc: "Personal Checking",
|
Desc: "Personal Checking",
|
||||||
Phone: "888-222-5827",
|
Phone: "888-222-5827",
|
||||||
BankAcctInfo: &bankacctinfo,
|
BankAcctInfo: &bankacctinfo,
|
||||||
@ -149,10 +148,11 @@ func TestUnmarshalAcctInfoResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
expected.Signup = append(expected.Signup, &acctInfoResponse)
|
expected.Signup = append(expected.Signup, &acctInfoResponse)
|
||||||
|
|
||||||
response, err := ofxgo.ParseResponse(responseReader)
|
response, err := ParseResponse(responseReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
t.Fatalf("Unexpected error unmarshalling response: %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkResponsesEqual(t, &expected, response)
|
checkResponsesEqual(t, &expected, response)
|
||||||
|
checkResponseRoundTrip(t, response)
|
||||||
}
|
}
|
||||||
|
165
types_test.go
165
types_test.go
@ -1,9 +1,8 @@
|
|||||||
package ofxgo_test
|
package ofxgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/aclindsa/xml"
|
"github.com/aclindsa/xml"
|
||||||
"github.com/aclindsa/ofxgo"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -57,7 +56,7 @@ func unmarshalHelper(t *testing.T, input string, expected interface{}, overwritt
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalInt(t *testing.T) {
|
func TestMarshalInt(t *testing.T) {
|
||||||
var i ofxgo.Int = 927
|
var i Int = 927
|
||||||
marshalHelper(t, "927", &i)
|
marshalHelper(t, "927", &i)
|
||||||
i = 0
|
i = 0
|
||||||
marshalHelper(t, "0", &i)
|
marshalHelper(t, "0", &i)
|
||||||
@ -66,7 +65,7 @@ func TestMarshalInt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalInt(t *testing.T) {
|
func TestUnmarshalInt(t *testing.T) {
|
||||||
var i, overwritten ofxgo.Int = -48394, 0
|
var i, overwritten Int = -48394, 0
|
||||||
unmarshalHelper(t, "-48394", &i, &overwritten)
|
unmarshalHelper(t, "-48394", &i, &overwritten)
|
||||||
i = 0
|
i = 0
|
||||||
unmarshalHelper(t, "0", &i, &overwritten)
|
unmarshalHelper(t, "0", &i, &overwritten)
|
||||||
@ -78,7 +77,7 @@ func TestUnmarshalInt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalAmount(t *testing.T) {
|
func TestMarshalAmount(t *testing.T) {
|
||||||
var a ofxgo.Amount
|
var a Amount
|
||||||
|
|
||||||
a.SetFrac64(8, 1)
|
a.SetFrac64(8, 1)
|
||||||
marshalHelper(t, "8", &a)
|
marshalHelper(t, "8", &a)
|
||||||
@ -95,13 +94,13 @@ func TestMarshalAmount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalAmount(t *testing.T) {
|
func TestUnmarshalAmount(t *testing.T) {
|
||||||
var a, overwritten ofxgo.Amount
|
var a, overwritten Amount
|
||||||
|
|
||||||
// Amount/big.Rat needs a special equality test because reflect.DeepEqual
|
// Amount/big.Rat needs a special equality test because reflect.DeepEqual
|
||||||
// doesn't always return equal for two values that big.Rat.Cmp() does
|
// doesn't always return equal for two values that big.Rat.Cmp() does
|
||||||
eq := func(a, b interface{}) bool {
|
eq := func(a, b interface{}) bool {
|
||||||
if amountA, ok := a.(*ofxgo.Amount); ok {
|
if amountA, ok := a.(*Amount); ok {
|
||||||
if amountB, ok2 := b.(*ofxgo.Amount); ok2 {
|
if amountB, ok2 := b.(*Amount); ok2 {
|
||||||
return amountA.Cmp(&amountB.Rat) == 0
|
return amountA.Cmp(&amountB.Rat) == 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,18 +126,18 @@ func TestUnmarshalAmount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAmountEqual(t *testing.T) {
|
func TestAmountEqual(t *testing.T) {
|
||||||
assertEq := func(a, b ofxgo.Amount) {
|
assertEq := func(a, b Amount) {
|
||||||
if !a.Equal(b) {
|
if !a.Equal(b) {
|
||||||
t.Fatalf("Amounts should be equal but Equal returned false: %s and %s\n", a, b)
|
t.Fatalf("Amounts should be equal but Equal returned false: %s and %s\n", a, b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertNEq := func(a, b ofxgo.Amount) {
|
assertNEq := func(a, b Amount) {
|
||||||
if a.Equal(b) {
|
if a.Equal(b) {
|
||||||
t.Fatalf("Amounts should not be equal but Equal returned true: %s and %s\n", a, b)
|
t.Fatalf("Amounts should not be equal but Equal returned true: %s and %s\n", a, b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var a, b ofxgo.Amount
|
var a, b Amount
|
||||||
a.SetInt64(-19487135)
|
a.SetInt64(-19487135)
|
||||||
b.SetInt64(-19487135)
|
b.SetInt64(-19487135)
|
||||||
assertEq(a, b)
|
assertEq(a, b)
|
||||||
@ -154,53 +153,53 @@ func TestAmountEqual(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalDate(t *testing.T) {
|
func TestMarshalDate(t *testing.T) {
|
||||||
var d *ofxgo.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)
|
||||||
NST := time.FixedZone("NST", -(3*60+30)*60)
|
NST := time.FixedZone("NST", -(3*60+30)*60)
|
||||||
|
|
||||||
d = ofxgo.NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
d = NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||||
marshalHelper(t, "20170314150926.053[0:GMT]", d)
|
marshalHelper(t, "20170314150926.053[0:GMT]", d)
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, NPT)
|
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, NPT)
|
||||||
marshalHelper(t, "20170314150926.053[5.75:NPT]", d)
|
marshalHelper(t, "20170314150926.053[5.75:NPT]", d)
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
||||||
marshalHelper(t, "20170314150926.053[-5:EST]", d)
|
marshalHelper(t, "20170314150926.053[-5:EST]", d)
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, UTC)
|
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, UTC)
|
||||||
marshalHelper(t, "20170314150926.053[0:UTC]", d)
|
marshalHelper(t, "20170314150926.053[0:UTC]", d)
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, IST)
|
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, IST)
|
||||||
marshalHelper(t, "20170314150926.053[5.50:IST]", d)
|
marshalHelper(t, "20170314150926.053[5.50:IST]", d)
|
||||||
d = ofxgo.NewDate(9999, 11, 1, 23, 59, 59, 1000, EST)
|
d = NewDate(9999, 11, 1, 23, 59, 59, 1000, EST)
|
||||||
marshalHelper(t, "99991101235959.000[-5:EST]", d)
|
marshalHelper(t, "99991101235959.000[-5:EST]", d)
|
||||||
d = ofxgo.NewDate(0, 1, 1, 0, 0, 0, 0, IST)
|
d = NewDate(0, 1, 1, 0, 0, 0, 0, IST)
|
||||||
marshalHelper(t, "00000101000000.000[5.50:IST]", d)
|
marshalHelper(t, "00000101000000.000[5.50:IST]", d)
|
||||||
d = &ofxgo.Date{Time: time.Unix(0, 0).In(UTC)}
|
d = &Date{Time: time.Unix(0, 0).In(UTC)}
|
||||||
marshalHelper(t, "19700101000000.000[0:UTC]", d)
|
marshalHelper(t, "19700101000000.000[0:UTC]", d)
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, EST)
|
d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, EST)
|
||||||
marshalHelper(t, "20170314000026.053[-5:EST]", d)
|
marshalHelper(t, "20170314000026.053[-5:EST]", d)
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST)
|
d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST)
|
||||||
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 = ofxgo.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalDate(t *testing.T) {
|
func TestUnmarshalDate(t *testing.T) {
|
||||||
var d *ofxgo.Date
|
var d *Date
|
||||||
var overwritten ofxgo.Date
|
var overwritten Date
|
||||||
GMT := time.FixedZone("GMT", 0)
|
GMT := time.FixedZone("GMT", 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)
|
||||||
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.(*ofxgo.Date); ok {
|
if dateA, ok := a.(*Date); ok {
|
||||||
if dateB, ok2 := b.(*ofxgo.Date); ok2 {
|
if dateB, ok2 := b.(*Date); ok2 {
|
||||||
return dateA.Equal(*dateB)
|
return dateA.Equal(*dateB)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,14 +207,14 @@ func TestUnmarshalDate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure omitted fields default to the correct values
|
// Ensure omitted fields default to the correct values
|
||||||
d = ofxgo.NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
d = NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||||
unmarshalHelper2(t, "20170314150926.053[0]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053[0]", d, &overwritten, eq)
|
||||||
unmarshalHelper2(t, "20170314150926.053", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053", d, &overwritten, eq)
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 0, 0, 0, 0, GMT)
|
d = NewDate(2017, 3, 14, 0, 0, 0, 0, GMT)
|
||||||
unmarshalHelper2(t, "20170314", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314", d, &overwritten, eq)
|
||||||
|
|
||||||
// Ensure all signs on time zone offsets are properly handled
|
// Ensure all signs on time zone offsets are properly handled
|
||||||
d = ofxgo.NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
d = NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||||
unmarshalHelper2(t, "20170314150926.053[0:GMT]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053[0:GMT]", d, &overwritten, eq)
|
||||||
unmarshalHelper2(t, "20170314150926.053[+0:GMT]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053[+0:GMT]", d, &overwritten, eq)
|
||||||
unmarshalHelper2(t, "20170314150926.053[-0:GMT]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053[-0:GMT]", d, &overwritten, eq)
|
||||||
@ -223,38 +222,38 @@ func TestUnmarshalDate(t *testing.T) {
|
|||||||
unmarshalHelper2(t, "20170314150926.053[+0]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053[+0]", d, &overwritten, eq)
|
||||||
unmarshalHelper2(t, "20170314150926.053[-0]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053[-0]", d, &overwritten, eq)
|
||||||
|
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, NPT)
|
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, NPT)
|
||||||
unmarshalHelper2(t, "20170314150926.053[5.75:NPT]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053[5.75:NPT]", d, &overwritten, eq)
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
||||||
unmarshalHelper2(t, "20170314150926.053[-5:EST]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053[-5:EST]", d, &overwritten, eq)
|
||||||
d = ofxgo.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:GMT]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053[0:GMT]", d, &overwritten, eq)
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, IST)
|
d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, IST)
|
||||||
unmarshalHelper2(t, "20170314150926.053[5.50:IST]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314150926.053[5.50:IST]", d, &overwritten, eq)
|
||||||
d = ofxgo.NewDate(2018, 11, 1, 23, 59, 58, 0, EST)
|
d = NewDate(2018, 11, 1, 23, 59, 58, 0, EST)
|
||||||
unmarshalHelper2(t, "20181101235958.000[-5:EST]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20181101235958.000[-5:EST]", d, &overwritten, eq)
|
||||||
d = ofxgo.NewDate(0, 1, 1, 0, 0, 0, 0, IST)
|
d = NewDate(0, 1, 1, 0, 0, 0, 0, IST)
|
||||||
unmarshalHelper2(t, "00000101000000.000[5.50:IST]", d, &overwritten, eq)
|
unmarshalHelper2(t, "00000101000000.000[5.50:IST]", d, &overwritten, eq)
|
||||||
d = &ofxgo.Date{Time: time.Unix(0, 0).In(GMT)}
|
d = &Date{Time: time.Unix(0, 0).In(GMT)}
|
||||||
unmarshalHelper2(t, "19700101000000.000[0:GMT]", d, &overwritten, eq)
|
unmarshalHelper2(t, "19700101000000.000[0:GMT]", d, &overwritten, eq)
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, EST)
|
d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, EST)
|
||||||
unmarshalHelper2(t, "20170314000026.053[-5:EST]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314000026.053[-5:EST]", d, &overwritten, eq)
|
||||||
d = ofxgo.NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST)
|
d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST)
|
||||||
unmarshalHelper2(t, "20170314000026.053[-3.50:NST]", d, &overwritten, eq)
|
unmarshalHelper2(t, "20170314000026.053[-3.50:NST]", d, &overwritten, eq)
|
||||||
|
|
||||||
// Autopopulate zone without textual description for GMT
|
// Autopopulate zone without textual description for GMT
|
||||||
d = ofxgo.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 = ofxgo.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)
|
||||||
d = ofxgo.NewDate(2016, 12, 7, 16, 0, 0, 0, EST)
|
d = NewDate(2016, 12, 7, 16, 0, 0, 0, EST)
|
||||||
unmarshalHelper2(t, "20161207160000.000[-5:EST]610900.500[-9:BST]", d, &overwritten, eq) // extra part intentionally different to ensure the first timezone is parsed
|
unmarshalHelper2(t, "20161207160000.000[-5:EST]610900.500[-9:BST]", d, &overwritten, eq) // extra part intentionally different to ensure the first timezone is parsed
|
||||||
|
|
||||||
// Make sure we properly handle ending newlines
|
// Make sure we properly handle ending newlines
|
||||||
d = ofxgo.NewDate(2018, 11, 1, 23, 59, 58, 0, EST)
|
d = NewDate(2018, 11, 1, 23, 59, 58, 0, EST)
|
||||||
unmarshalHelper2(t, "20181101235958.000[-5:EST]\n", d, &overwritten, eq)
|
unmarshalHelper2(t, "20181101235958.000[-5:EST]\n", d, &overwritten, eq)
|
||||||
unmarshalHelper2(t, "20181101235958.000[-5:EST]\n\t", d, &overwritten, eq)
|
unmarshalHelper2(t, "20181101235958.000[-5:EST]\n\t", d, &overwritten, eq)
|
||||||
}
|
}
|
||||||
@ -263,23 +262,23 @@ func TestDateEqual(t *testing.T) {
|
|||||||
GMT := time.FixedZone("GMT", 0)
|
GMT := time.FixedZone("GMT", 0)
|
||||||
EST := time.FixedZone("EST", -5*60*60)
|
EST := time.FixedZone("EST", -5*60*60)
|
||||||
|
|
||||||
assertEq := func(a, b *ofxgo.Date) {
|
assertEq := func(a, b *Date) {
|
||||||
if !a.Equal(*b) {
|
if !a.Equal(*b) {
|
||||||
t.Fatalf("Dates should be equal but Equal returned false: %s and %s\n", *a, *b)
|
t.Fatalf("Dates should be equal but Equal returned false: %s and %s\n", *a, *b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertNEq := func(a, b *ofxgo.Date) {
|
assertNEq := func(a, b *Date) {
|
||||||
if a.Equal(*b) {
|
if a.Equal(*b) {
|
||||||
t.Fatalf("Dates should not be equal but Equal returned true: %s and %s\n", *a, *b)
|
t.Fatalf("Dates should not be equal but Equal returned true: %s and %s\n", *a, *b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure omitted fields default to the correct values
|
// Ensure omitted fields default to the correct values
|
||||||
gmt1 := ofxgo.NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
gmt1 := NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000)
|
||||||
gmt2 := ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT)
|
gmt2 := NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT)
|
||||||
est1 := ofxgo.NewDate(2017, 3, 14, 10, 9, 26, 53*1000*1000, EST)
|
est1 := NewDate(2017, 3, 14, 10, 9, 26, 53*1000*1000, EST)
|
||||||
est2 := ofxgo.NewDate(2017, 3, 14, 10, 9, 26, 53*1000*1000+1, EST)
|
est2 := NewDate(2017, 3, 14, 10, 9, 26, 53*1000*1000+1, EST)
|
||||||
est3 := ofxgo.NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
est3 := NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST)
|
||||||
|
|
||||||
assertEq(gmt1, gmt2)
|
assertEq(gmt1, gmt2)
|
||||||
assertEq(gmt2, gmt1)
|
assertEq(gmt2, gmt1)
|
||||||
@ -291,7 +290,7 @@ func TestDateEqual(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalString(t *testing.T) {
|
func TestMarshalString(t *testing.T) {
|
||||||
var s ofxgo.String = ""
|
var s String = ""
|
||||||
marshalHelper(t, "", &s)
|
marshalHelper(t, "", &s)
|
||||||
s = "foo&bar"
|
s = "foo&bar"
|
||||||
marshalHelper(t, "foo&bar", &s)
|
marshalHelper(t, "foo&bar", &s)
|
||||||
@ -302,7 +301,7 @@ func TestMarshalString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalString(t *testing.T) {
|
func TestUnmarshalString(t *testing.T) {
|
||||||
var s, overwritten ofxgo.String = "", ""
|
var s, overwritten String = "", ""
|
||||||
unmarshalHelper(t, "", &s, &overwritten)
|
unmarshalHelper(t, "", &s, &overwritten)
|
||||||
s = "foo&bar"
|
s = "foo&bar"
|
||||||
unmarshalHelper(t, "foo&bar", &s, &overwritten)
|
unmarshalHelper(t, "foo&bar", &s, &overwritten)
|
||||||
@ -317,15 +316,22 @@ func TestUnmarshalString(t *testing.T) {
|
|||||||
unmarshalHelper(t, "Some Name\n ", &s, &overwritten)
|
unmarshalHelper(t, "Some Name\n ", &s, &overwritten)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStringString(t *testing.T) {
|
||||||
|
var s String = "foobar"
|
||||||
|
if s.String() != "foobar" {
|
||||||
|
t.Fatalf("Unexpected result when returning String.String(): %s\n", s.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarshalBoolean(t *testing.T) {
|
func TestMarshalBoolean(t *testing.T) {
|
||||||
var b ofxgo.Boolean = true
|
var b Boolean = true
|
||||||
marshalHelper(t, "Y", &b)
|
marshalHelper(t, "Y", &b)
|
||||||
b = false
|
b = false
|
||||||
marshalHelper(t, "N", &b)
|
marshalHelper(t, "N", &b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalBoolean(t *testing.T) {
|
func TestUnmarshalBoolean(t *testing.T) {
|
||||||
var b, overwritten ofxgo.Boolean = true, false
|
var b, overwritten Boolean = true, false
|
||||||
unmarshalHelper(t, "Y", &b, &overwritten)
|
unmarshalHelper(t, "Y", &b, &overwritten)
|
||||||
b = false
|
b = false
|
||||||
unmarshalHelper(t, "N", &b, &overwritten)
|
unmarshalHelper(t, "N", &b, &overwritten)
|
||||||
@ -334,13 +340,24 @@ func TestUnmarshalBoolean(t *testing.T) {
|
|||||||
unmarshalHelper(t, "N\n \t", &b, &overwritten)
|
unmarshalHelper(t, "N\n \t", &b, &overwritten)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStringBoolean(t *testing.T) {
|
||||||
|
var b Boolean = true
|
||||||
|
if b.String() != "true" {
|
||||||
|
t.Fatalf("Unexpected string for Boolean.String() for true: %s\n", b.String())
|
||||||
|
}
|
||||||
|
b = false
|
||||||
|
if b.String() != "false" {
|
||||||
|
t.Fatalf("Unexpected string for Boolean.String() for false: %s\n", b.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarshalUID(t *testing.T) {
|
func TestMarshalUID(t *testing.T) {
|
||||||
var u ofxgo.UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04"
|
var u UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04"
|
||||||
marshalHelper(t, "d1cf3d3d-9ef9-4a97-b180-81706829cb04", &u)
|
marshalHelper(t, "d1cf3d3d-9ef9-4a97-b180-81706829cb04", &u)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalUID(t *testing.T) {
|
func TestUnmarshalUID(t *testing.T) {
|
||||||
var u, overwritten ofxgo.UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04", ""
|
var u, overwritten UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04", ""
|
||||||
unmarshalHelper(t, "d1cf3d3d-9ef9-4a97-b180-81706829cb04", &u, &overwritten)
|
unmarshalHelper(t, "d1cf3d3d-9ef9-4a97-b180-81706829cb04", &u, &overwritten)
|
||||||
// Make sure stray newlines are handled properly
|
// Make sure stray newlines are handled properly
|
||||||
u = "0f94ce83-13b7-7568-e4fc-c02c7b47e7ab"
|
u = "0f94ce83-13b7-7568-e4fc-c02c7b47e7ab"
|
||||||
@ -349,7 +366,7 @@ func TestUnmarshalUID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUIDRecommendedFormat(t *testing.T) {
|
func TestUIDRecommendedFormat(t *testing.T) {
|
||||||
var u ofxgo.UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04"
|
var u UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04"
|
||||||
if ok, err := u.RecommendedFormat(); !ok || err != nil {
|
if ok, err := u.RecommendedFormat(); !ok || err != nil {
|
||||||
t.Fatalf("UID unexpectedly failed validation\n")
|
t.Fatalf("UID unexpectedly failed validation\n")
|
||||||
}
|
}
|
||||||
@ -368,7 +385,7 @@ func TestUIDRecommendedFormat(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUIDValid(t *testing.T) {
|
func TestUIDValid(t *testing.T) {
|
||||||
var u ofxgo.UID = ""
|
var u UID = ""
|
||||||
if ok, err := u.Valid(); ok || err == nil {
|
if ok, err := u.Valid(); ok || err == nil {
|
||||||
t.Fatalf("Empty UID unexpectedly valid\n")
|
t.Fatalf("Empty UID unexpectedly valid\n")
|
||||||
}
|
}
|
||||||
@ -383,7 +400,7 @@ func TestUIDValid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRandomUID(t *testing.T) {
|
func TestRandomUID(t *testing.T) {
|
||||||
uid, err := ofxgo.RandomUID()
|
uid, err := RandomUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error when calling RandomUID: %s\n", err)
|
t.Fatalf("Unexpected error when calling RandomUID: %s\n", err)
|
||||||
}
|
}
|
||||||
@ -393,46 +410,46 @@ func TestRandomUID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalCurrSymbol(t *testing.T) {
|
func TestMarshalCurrSymbol(t *testing.T) {
|
||||||
c, _ := ofxgo.NewCurrSymbol("USD")
|
c, _ := NewCurrSymbol("USD")
|
||||||
marshalHelper(t, "USD", &c)
|
marshalHelper(t, "USD", &c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalCurrSymbol(t *testing.T) {
|
func TestUnmarshalCurrSymbol(t *testing.T) {
|
||||||
var overwritten ofxgo.CurrSymbol
|
var overwritten CurrSymbol
|
||||||
c, _ := ofxgo.NewCurrSymbol("USD")
|
c, _ := NewCurrSymbol("USD")
|
||||||
unmarshalHelper(t, "USD", c, &overwritten)
|
unmarshalHelper(t, "USD", c, &overwritten)
|
||||||
// Make sure stray newlines are handled properly
|
// Make sure stray newlines are handled properly
|
||||||
c, _ = ofxgo.NewCurrSymbol("EUR")
|
c, _ = NewCurrSymbol("EUR")
|
||||||
unmarshalHelper(t, "EUR\n", c, &overwritten)
|
unmarshalHelper(t, "EUR\n", c, &overwritten)
|
||||||
unmarshalHelper(t, "EUR\n\t", c, &overwritten)
|
unmarshalHelper(t, "EUR\n\t", c, &overwritten)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCurrSymbolEqual(t *testing.T) {
|
func TestCurrSymbolEqual(t *testing.T) {
|
||||||
usd1, _ := ofxgo.NewCurrSymbol("USD")
|
usd1, _ := NewCurrSymbol("USD")
|
||||||
usd2, _ := ofxgo.NewCurrSymbol("USD")
|
usd2, _ := NewCurrSymbol("USD")
|
||||||
if !usd1.Equal(*usd2) {
|
if !usd1.Equal(*usd2) {
|
||||||
t.Fatalf("Two \"USD\" CurrSymbols returned !Equal()\n")
|
t.Fatalf("Two \"USD\" CurrSymbols returned !Equal()\n")
|
||||||
}
|
}
|
||||||
xxx, _ := ofxgo.NewCurrSymbol("XXX")
|
xxx, _ := NewCurrSymbol("XXX")
|
||||||
if usd1.Equal(*xxx) {
|
if usd1.Equal(*xxx) {
|
||||||
t.Fatalf("\"USD\" and \"XXX\" CurrSymbols returned Equal()\n")
|
t.Fatalf("\"USD\" and \"XXX\" CurrSymbols returned Equal()\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCurrSymbolValid(t *testing.T) {
|
func TestCurrSymbolValid(t *testing.T) {
|
||||||
var initial ofxgo.CurrSymbol
|
var initial CurrSymbol
|
||||||
ok, err := initial.Valid()
|
ok, err := initial.Valid()
|
||||||
if ok || err == nil {
|
if ok || err == nil {
|
||||||
t.Fatalf("CurrSymbol unexpectedly returned Valid() for initial value\n")
|
t.Fatalf("CurrSymbol unexpectedly returned Valid() for initial value\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
ars, _ := ofxgo.NewCurrSymbol("ARS")
|
ars, _ := NewCurrSymbol("ARS")
|
||||||
ok, err = ars.Valid()
|
ok, err = ars.Valid()
|
||||||
if !ok || err != nil {
|
if !ok || err != nil {
|
||||||
t.Fatalf("CurrSymbol unexpectedly returned !Valid() for \"ARS\": %s\n", err.Error())
|
t.Fatalf("CurrSymbol unexpectedly returned !Valid() for \"ARS\": %s\n", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
xxx, _ := ofxgo.NewCurrSymbol("XXX")
|
xxx, _ := NewCurrSymbol("XXX")
|
||||||
ok, err = xxx.Valid()
|
ok, err = xxx.Valid()
|
||||||
if ok || err == nil {
|
if ok || err == nil {
|
||||||
t.Fatalf("CurrSymbol unexpectedly returned Valid() for \"XXX\"\n")
|
t.Fatalf("CurrSymbol unexpectedly returned Valid() for \"XXX\"\n")
|
||||||
@ -440,21 +457,21 @@ func TestCurrSymbolValid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewCurrSymbol(t *testing.T) {
|
func TestNewCurrSymbol(t *testing.T) {
|
||||||
curr, err := ofxgo.NewCurrSymbol("GBP")
|
curr, err := NewCurrSymbol("GBP")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error calling NewCurrSymbol: %s\n", err)
|
t.Fatalf("Unexpected error calling NewCurrSymbol: %s\n", err)
|
||||||
}
|
}
|
||||||
if curr.String() != "GBP" {
|
if curr.String() != "GBP" {
|
||||||
t.Fatalf("Created CurrSymbol doesn't print \"GBP\" as string representation\n")
|
t.Fatalf("Created CurrSymbol doesn't print \"GBP\" as string representation\n")
|
||||||
}
|
}
|
||||||
curr, err = ofxgo.NewCurrSymbol("AFN")
|
curr, err = NewCurrSymbol("AFN")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unexpected error calling NewCurrSymbol: %s\n", err)
|
t.Fatalf("Unexpected error calling NewCurrSymbol: %s\n", err)
|
||||||
}
|
}
|
||||||
if curr.String() != "AFN" {
|
if curr.String() != "AFN" {
|
||||||
t.Fatalf("Created CurrSymbol doesn't print \"AFN\" as string representation\n")
|
t.Fatalf("Created CurrSymbol doesn't print \"AFN\" as string representation\n")
|
||||||
}
|
}
|
||||||
curr, err = ofxgo.NewCurrSymbol("BLAH")
|
curr, err = NewCurrSymbol("BLAH")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("NewCurrSymbol didn't error on invalid currency identifier\n")
|
t.Fatalf("NewCurrSymbol didn't error on invalid currency identifier\n")
|
||||||
}
|
}
|
||||||
|
83
vanguard_client.go
Normal file
83
vanguard_client.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package ofxgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VanguardClient provides a Client implementation which handles Vanguard's
|
||||||
|
// cookie-passing requirements. VanguardClient uses default, non-zero settings,
|
||||||
|
// if its fields are not initialized.
|
||||||
|
type VanguardClient struct {
|
||||||
|
*BasicClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVanguardClient returns a Client interface configured to handle Vanguard's
|
||||||
|
// brand of idiosyncrasy
|
||||||
|
func NewVanguardClient(bc *BasicClient) Client {
|
||||||
|
return &VanguardClient{bc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawRequestCookies is RawRequest with the added feature of sending cookies
|
||||||
|
func rawRequestCookies(URL string, r io.Reader, cookies []*http.Cookie) (*http.Response, error) {
|
||||||
|
if !strings.HasPrefix(URL, "https://") {
|
||||||
|
return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := http.NewRequest("POST", URL, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
request.Header.Set("Content-Type", "application/x-ofx")
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
request.AddCookie(cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := http.DefaultClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return nil, errors.New("OFXQuery request status: " + response.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
r.SetClientFields(c)
|
||||||
|
|
||||||
|
b, err := r.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := c.RawRequest(r.URL, b)
|
||||||
|
|
||||||
|
// Some financial institutions (cough, Vanguard, cough), require a cookie
|
||||||
|
// to be set on the http request, or they return empty responses.
|
||||||
|
// 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,
|
||||||
|
// re-try the request while sending their cookies back to them.
|
||||||
|
if response != nil && response.ContentLength <= 0 && len(response.Cookies()) > 0 {
|
||||||
|
b, err = r.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawRequestCookies(r.URL, b, response.Cookies())
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return clientRequest(c, r)
|
||||||
|
}
|
Reference in New Issue
Block a user